From 13aaba0f1d10f03cdd2ab3f06aeda4c26f86e9e8 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Fri, 25 Jul 2025 22:10:23 -0600 Subject: [PATCH 01/36] chore: simplifies type --- src/Tools/DTO/FunctionDeclaration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index 435ca021..2ea03377 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -38,7 +38,7 @@ class FunctionDeclaration implements WithJsonSchemaInterface * * @param string $name The name of the function. * @param string $description A description of what the function does. - * @param mixed|null $parameters The JSON schema for the function parameters. + * @param mixed $parameters The JSON schema for the function parameters. */ public function __construct(string $name, string $description, $parameters = null) { From 0b52ffd57ed4b7e9ec63bc0659a155302ee2dfc9 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 09:23:11 -0600 Subject: [PATCH 02/36] feat: adds json serialization to DTOs --- .../Contracts/WithJsonSerialization.php | 25 +++++++++ src/Files/DTO/File.php | 44 ++++++++++++++- src/Messages/DTO/Message.php | 37 ++++++++++++- src/Messages/DTO/MessagePart.php | 55 ++++++++++++++++++- src/Messages/DTO/ModelMessage.php | 16 ++++++ src/Messages/DTO/SystemMessage.php | 16 ++++++ src/Messages/DTO/UserMessage.php | 16 ++++++ .../Contracts/OperationInterface.php | 3 +- src/Operations/DTO/GenerativeAiOperation.php | 39 +++++++++++++ src/Results/Contracts/ResultInterface.php | 3 +- src/Results/DTO/Candidate.php | 36 +++++++++++- src/Results/DTO/GenerativeAiResult.php | 45 +++++++++++++++ src/Results/DTO/TokenUsage.php | 33 ++++++++++- src/Tools/DTO/FunctionCall.php | 46 +++++++++++++++- src/Tools/DTO/FunctionDeclaration.php | 38 ++++++++++++- src/Tools/DTO/FunctionResponse.php | 33 ++++++++++- src/Tools/DTO/Tool.php | 50 ++++++++++++++++- src/Tools/DTO/WebSearch.php | 36 +++++++++++- 18 files changed, 559 insertions(+), 12 deletions(-) create mode 100644 src/Common/Contracts/WithJsonSerialization.php diff --git a/src/Common/Contracts/WithJsonSerialization.php b/src/Common/Contracts/WithJsonSerialization.php new file mode 100644 index 00000000..73f98da2 --- /dev/null +++ b/src/Common/Contracts/WithJsonSerialization.php @@ -0,0 +1,25 @@ + $json The JSON data. + * @return self The created instance. + */ + public static function fromJson(array $json); +} diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index cc84724f..3fe47a45 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Files\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; use WordPress\AiClient\Files\Enums\FileTypeEnum; use WordPress\AiClient\Files\ValueObjects\MimeType; @@ -16,7 +17,7 @@ * * @since n.e.x.t */ -class File implements WithJsonSchemaInterface +class File implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var MimeType The MIME type of the file. @@ -377,4 +378,45 @@ public static function getJsonSchema(): array ], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + $data = [ + 'fileType' => $this->fileType->value, + 'mimeType' => $this->getMimeType(), + ]; + + if ($this->fileType->isRemote()) { + $data['url'] = $this->url; + } else { + $data['base64Data'] = $this->base64Data; + } + + return $data; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): File + { + $fileType = FileTypeEnum::from((string) $json['fileType']); + + if ($fileType->isRemote()) { + return new self((string) $json['url'], (string) $json['mimeType']); + } else { + // Create data URI from base64 data and mime type + $dataUri = sprintf('data:%s;base64,%s', (string) $json['mimeType'], (string) $json['base64Data']); + return new self($dataUri); + } + } } diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index cf73e30a..6dce8a1c 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Messages\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; /** @@ -15,7 +16,7 @@ * * @since n.e.x.t */ -class Message implements WithJsonSchemaInterface +class Message implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var MessageRoleEnum The role of the message sender. @@ -90,4 +91,38 @@ public static function getJsonSchema(): array 'required' => ['role', 'parts'], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'role' => $this->role->value, + 'parts' => array_map(function (MessagePart $part) { + return $part->jsonSerialize(); + }, $this->parts), + ]; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): Message + { + $role = MessageRoleEnum::from((string) $json['role']); + /** @var array> $partsData */ + $partsData = $json['parts']; + $parts = array_map(function (array $partData) { + return MessagePart::fromJson($partData); + }, $partsData); + + return new self($role, $parts); + } } diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index ddaf3945..9971b1b8 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Messages\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Tools\DTO\FunctionCall; @@ -18,7 +19,7 @@ * * @since n.e.x.t */ -class MessagePart implements WithJsonSchemaInterface +class MessagePart implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var MessagePartTypeEnum The type of this message part. @@ -202,4 +203,56 @@ public static function getJsonSchema(): array ], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + $data = ['type' => $this->type->value]; + + if ($this->type->isText() && $this->text !== null) { + $data['text'] = $this->text; + } elseif ($this->type->isFile() && $this->file !== null) { + $data['file'] = $this->file->jsonSerialize(); + } elseif ($this->type->isFunctionCall() && $this->functionCall !== null) { + $data['functionCall'] = $this->functionCall->jsonSerialize(); + } elseif ($this->type->isFunctionResponse() && $this->functionResponse !== null) { + $data['functionResponse'] = $this->functionResponse->jsonSerialize(); + } + + return $data; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): MessagePart + { + $type = MessagePartTypeEnum::from((string) $json['type']); + + if ($type->isText()) { + return new self((string) $json['text']); + } elseif ($type->isFile()) { + /** @var array $fileData */ + $fileData = $json['file']; + return new self(File::fromJson($fileData)); + } elseif ($type->isFunctionCall()) { + /** @var array $functionCallData */ + $functionCallData = $json['functionCall']; + return new self(FunctionCall::fromJson($functionCallData)); + } elseif ($type->isFunctionResponse()) { + /** @var array $functionResponseData */ + $functionResponseData = $json['functionResponse']; + return new self(FunctionResponse::fromJson($functionResponseData)); + } + + throw new \InvalidArgumentException(sprintf('Unknown message part type: %s', $json['type'])); + } } diff --git a/src/Messages/DTO/ModelMessage.php b/src/Messages/DTO/ModelMessage.php index cf67b79c..239029f7 100644 --- a/src/Messages/DTO/ModelMessage.php +++ b/src/Messages/DTO/ModelMessage.php @@ -27,4 +27,20 @@ public function __construct(array $parts) { parent::__construct(MessageRoleEnum::model(), $parts); } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): ModelMessage + { + /** @var array> $partsData */ + $partsData = $json['parts']; + $parts = array_map(function (array $partData) { + return MessagePart::fromJson($partData); + }, $partsData); + + return new self($parts); + } } diff --git a/src/Messages/DTO/SystemMessage.php b/src/Messages/DTO/SystemMessage.php index e2adebf5..3f1c3f5b 100644 --- a/src/Messages/DTO/SystemMessage.php +++ b/src/Messages/DTO/SystemMessage.php @@ -27,4 +27,20 @@ public function __construct(array $parts) { parent::__construct(MessageRoleEnum::system(), $parts); } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): SystemMessage + { + /** @var array> $partsData */ + $partsData = $json['parts']; + $parts = array_map(function (array $partData) { + return MessagePart::fromJson($partData); + }, $partsData); + + return new self($parts); + } } diff --git a/src/Messages/DTO/UserMessage.php b/src/Messages/DTO/UserMessage.php index c0cb931f..859303cd 100644 --- a/src/Messages/DTO/UserMessage.php +++ b/src/Messages/DTO/UserMessage.php @@ -26,4 +26,20 @@ public function __construct(array $parts) { parent::__construct(MessageRoleEnum::user(), $parts); } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): UserMessage + { + /** @var array> $partsData */ + $partsData = $json['parts']; + $parts = array_map(function (array $partData) { + return MessagePart::fromJson($partData); + }, $partsData); + + return new self($parts); + } } diff --git a/src/Operations/Contracts/OperationInterface.php b/src/Operations/Contracts/OperationInterface.php index d9bfa6b5..4fa9ab7a 100644 --- a/src/Operations/Contracts/OperationInterface.php +++ b/src/Operations/Contracts/OperationInterface.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Operations\Contracts; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; use WordPress\AiClient\Operations\Enums\OperationStateEnum; /** @@ -15,7 +16,7 @@ * * @since n.e.x.t */ -interface OperationInterface extends WithJsonSchemaInterface +interface OperationInterface extends WithJsonSchemaInterface, WithJsonSerialization { /** * Gets the operation ID. diff --git a/src/Operations/DTO/GenerativeAiOperation.php b/src/Operations/DTO/GenerativeAiOperation.php index 9ab109b2..3cd88c47 100644 --- a/src/Operations/DTO/GenerativeAiOperation.php +++ b/src/Operations/DTO/GenerativeAiOperation.php @@ -132,4 +132,43 @@ public static function getJsonSchema(): array ], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + $data = [ + 'id' => $this->id, + 'state' => $this->state->value, + ]; + + if ($this->result !== null) { + $data['result'] = $this->result->jsonSerialize(); + } + + return $data; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): GenerativeAiOperation + { + $state = OperationStateEnum::from((string) $json['state']); + $result = null; + if (isset($json['result'])) { + /** @var array $resultData */ + $resultData = $json['result']; + $result = GenerativeAiResult::fromJson($resultData); + } + + return new self((string) $json['id'], $state, $result); + } } diff --git a/src/Results/Contracts/ResultInterface.php b/src/Results/Contracts/ResultInterface.php index 4da83eab..49140e07 100644 --- a/src/Results/Contracts/ResultInterface.php +++ b/src/Results/Contracts/ResultInterface.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Results\Contracts; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; use WordPress\AiClient\Results\DTO\TokenUsage; /** @@ -15,7 +16,7 @@ * * @since n.e.x.t */ -interface ResultInterface extends WithJsonSchemaInterface +interface ResultInterface extends WithJsonSchemaInterface, WithJsonSerialization { /** * Gets the result ID. diff --git a/src/Results/DTO/Candidate.php b/src/Results/DTO/Candidate.php index bdb9360d..b713fde4 100644 --- a/src/Results/DTO/Candidate.php +++ b/src/Results/DTO/Candidate.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Results\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; use WordPress\AiClient\Messages\DTO\Message; use WordPress\AiClient\Results\Enums\FinishReasonEnum; @@ -16,7 +17,7 @@ * * @since n.e.x.t */ -class Candidate implements WithJsonSchemaInterface +class Candidate implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var Message The generated message. @@ -115,4 +116,37 @@ public static function getJsonSchema(): array 'required' => ['message', 'finishReason', 'tokenCount'], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'message' => $this->message->jsonSerialize(), + 'finishReason' => $this->finishReason->value, + 'tokenCount' => $this->tokenCount, + ]; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): Candidate + { + /** @var array $messageData */ + $messageData = $json['message']; + + return new self( + Message::fromJson($messageData), + FinishReasonEnum::from((string) $json['finishReason']), + (int) $json['tokenCount'] + ); + } } diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index b9a19d5b..acae7be1 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -378,4 +378,49 @@ public static function getJsonSchema(): array 'required' => ['id', 'candidates', 'tokenUsage'], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'candidates' => array_map(function (Candidate $candidate) { + return $candidate->jsonSerialize(); + }, $this->candidates), + 'tokenUsage' => $this->tokenUsage->jsonSerialize(), + 'providerMetadata' => $this->providerMetadata, + ]; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): GenerativeAiResult + { + /** @var array> $candidatesData */ + $candidatesData = $json['candidates']; + $candidates = array_map(function (array $candidateData) { + return Candidate::fromJson($candidateData); + }, $candidatesData); + + /** @var array $tokenUsageData */ + $tokenUsageData = $json['tokenUsage']; + /** @var array $providerMetadata */ + $providerMetadata = $json['providerMetadata'] ?? []; + + return new self( + (string) $json['id'], + $candidates, + TokenUsage::fromJson($tokenUsageData), + $providerMetadata + ); + } } diff --git a/src/Results/DTO/TokenUsage.php b/src/Results/DTO/TokenUsage.php index da7d3714..0c7f5a04 100644 --- a/src/Results/DTO/TokenUsage.php +++ b/src/Results/DTO/TokenUsage.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Results\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; /** * Represents token usage statistics for an AI operation. @@ -14,7 +15,7 @@ * * @since n.e.x.t */ -class TokenUsage implements WithJsonSchemaInterface +class TokenUsage implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var int Number of tokens in the prompt. @@ -109,4 +110,34 @@ public static function getJsonSchema(): array 'required' => ['promptTokens', 'completionTokens', 'totalTokens'], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'promptTokens' => $this->promptTokens, + 'completionTokens' => $this->completionTokens, + 'totalTokens' => $this->totalTokens, + ]; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): TokenUsage + { + return new self( + (int) $json['promptTokens'], + (int) $json['completionTokens'], + (int) $json['totalTokens'] + ); + } } diff --git a/src/Tools/DTO/FunctionCall.php b/src/Tools/DTO/FunctionCall.php index bcbcfd3b..391ff368 100644 --- a/src/Tools/DTO/FunctionCall.php +++ b/src/Tools/DTO/FunctionCall.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Tools\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; /** * Represents a function call request from an AI model. @@ -14,7 +15,7 @@ * * @since n.e.x.t */ -class FunctionCall implements WithJsonSchemaInterface +class FunctionCall implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var string|null Unique identifier for this function call. @@ -125,4 +126,47 @@ public static function getJsonSchema(): array ], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + $data = []; + + if ($this->id !== null) { + $data['id'] = $this->id; + } + + if ($this->name !== null) { + $data['name'] = $this->name; + } + + if (!empty($this->args)) { + $data['args'] = $this->args; + } + + return $data; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): FunctionCall + { + /** @var array $args */ + $args = $json['args'] ?? []; + + return new self( + isset($json['id']) ? (string) $json['id'] : null, + isset($json['name']) ? (string) $json['name'] : null, + $args + ); + } } diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index 2ea03377..19e078dd 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Tools\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; /** * Represents a function declaration for AI models. @@ -14,7 +15,7 @@ * * @since n.e.x.t */ -class FunctionDeclaration implements WithJsonSchemaInterface +class FunctionDeclaration implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var string The name of the function. @@ -109,4 +110,39 @@ public static function getJsonSchema(): array 'required' => ['name', 'description'], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + $data = [ + 'name' => $this->name, + 'description' => $this->description, + ]; + + if ($this->parameters !== null) { + $data['parameters'] = $this->parameters; + } + + return $data; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): FunctionDeclaration + { + return new self( + (string) $json['name'], + (string) $json['description'], + $json['parameters'] ?? null + ); + } } diff --git a/src/Tools/DTO/FunctionResponse.php b/src/Tools/DTO/FunctionResponse.php index 42a1e617..94f30a0d 100644 --- a/src/Tools/DTO/FunctionResponse.php +++ b/src/Tools/DTO/FunctionResponse.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Tools\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; /** * Represents a response to a function call. @@ -14,7 +15,7 @@ * * @since n.e.x.t */ -class FunctionResponse implements WithJsonSchemaInterface +class FunctionResponse implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var string The ID of the function call this is responding to. @@ -109,4 +110,34 @@ public static function getJsonSchema(): array 'required' => ['id', 'name', 'response'], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'response' => $this->response, + ]; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): FunctionResponse + { + return new self( + (string) $json['id'], + (string) $json['name'], + $json['response'] + ); + } } diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index ccc2d108..f6041dbe 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Tools\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; use WordPress\AiClient\Providers\Enums\ToolTypeEnum; /** @@ -15,7 +16,7 @@ * * @since n.e.x.t */ -class Tool implements WithJsonSchemaInterface +class Tool implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var ToolTypeEnum The type of tool. @@ -132,4 +133,51 @@ public static function getJsonSchema(): array ], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + $data = ['type' => $this->type->value]; + + if ($this->type->isFunctionDeclarations() && $this->functionDeclarations !== null) { + $data['functionDeclarations'] = array_map(function (FunctionDeclaration $declaration) { + return $declaration->jsonSerialize(); + }, $this->functionDeclarations); + } elseif ($this->type->isWebSearch() && $this->webSearch !== null) { + $data['webSearch'] = $this->webSearch->jsonSerialize(); + } + + return $data; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): Tool + { + $type = ToolTypeEnum::from((string) $json['type']); + + if ($type->isFunctionDeclarations()) { + /** @var array> $declarationsData */ + $declarationsData = $json['functionDeclarations']; + $declarations = array_map(function (array $declarationData) { + return FunctionDeclaration::fromJson($declarationData); + }, $declarationsData); + return new self($declarations); + } elseif ($type->isWebSearch()) { + /** @var array $webSearchData */ + $webSearchData = $json['webSearch']; + return new self(WebSearch::fromJson($webSearchData)); + } + + throw new \InvalidArgumentException(sprintf('Unknown tool type: %s', $json['type'])); + } } diff --git a/src/Tools/DTO/WebSearch.php b/src/Tools/DTO/WebSearch.php index ecc1526f..02eb3e5f 100644 --- a/src/Tools/DTO/WebSearch.php +++ b/src/Tools/DTO/WebSearch.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Tools\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Common\Contracts\WithJsonSerialization; /** * Represents web search configuration for AI models. @@ -14,7 +15,7 @@ * * @since n.e.x.t */ -class WebSearch implements WithJsonSchemaInterface +class WebSearch implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var string[] List of domains that are allowed for web search. @@ -92,4 +93,37 @@ public static function getJsonSchema(): array 'required' => [], ]; } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'allowedDomains' => $this->allowedDomains, + 'disallowedDomains' => $this->disallowedDomains, + ]; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromJson(array $json): WebSearch + { + /** @var string[] $allowedDomains */ + $allowedDomains = $json['allowedDomains'] ?? []; + /** @var string[] $disallowedDomains */ + $disallowedDomains = $json['disallowedDomains'] ?? []; + + return new self( + $allowedDomains, + $disallowedDomains + ); + } } From 0c0407c486a78dd3422aa61acd45aa135dd751f8 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 09:37:07 -0600 Subject: [PATCH 03/36] refactor: moves enum trait to traits directory --- tests/{unit => traits}/EnumTestTrait.php | 2 +- tests/unit/Files/Enums/FileTypeEnumTest.php | 2 +- tests/unit/Messages/Enums/MessagePartTypeEnumTest.php | 2 +- tests/unit/Messages/Enums/MessageRoleEnumTest.php | 2 +- tests/unit/Messages/Enums/ModalityEnumTest.php | 2 +- tests/unit/Operations/Enums/OperationStateEnumTest.php | 2 +- tests/unit/Providers/Enums/ProviderTypeEnumTest.php | 2 +- tests/unit/Providers/Enums/ToolTypeEnumTest.php | 2 +- tests/unit/Providers/Models/Enums/CapabilityEnumTest.php | 2 +- tests/unit/Providers/Models/Enums/OptionEnumTest.php | 2 +- tests/unit/Results/Enums/FinishReasonEnumTest.php | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) rename tests/{unit => traits}/EnumTestTrait.php (98%) diff --git a/tests/unit/EnumTestTrait.php b/tests/traits/EnumTestTrait.php similarity index 98% rename from tests/unit/EnumTestTrait.php rename to tests/traits/EnumTestTrait.php index b73c16cb..d84703ee 100644 --- a/tests/unit/EnumTestTrait.php +++ b/tests/traits/EnumTestTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WordPress\AiClient\Tests\unit; +namespace WordPress\AiClient\Tests\traits; use WordPress\AiClient\Common\AbstractEnum; diff --git a/tests/unit/Files/Enums/FileTypeEnumTest.php b/tests/unit/Files/Enums/FileTypeEnumTest.php index c80101d2..3bd832ff 100644 --- a/tests/unit/Files/Enums/FileTypeEnumTest.php +++ b/tests/unit/Files/Enums/FileTypeEnumTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Files\Enums\FileTypeEnum; -use WordPress\AiClient\Tests\unit\EnumTestTrait; +use WordPress\AiClient\Tests\traits\EnumTestTrait; /** * @covers \WordPress\AiClient\Files\Enums\FileTypeEnum diff --git a/tests/unit/Messages/Enums/MessagePartTypeEnumTest.php b/tests/unit/Messages/Enums/MessagePartTypeEnumTest.php index e0551d24..35860816 100644 --- a/tests/unit/Messages/Enums/MessagePartTypeEnumTest.php +++ b/tests/unit/Messages/Enums/MessagePartTypeEnumTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; -use WordPress\AiClient\Tests\unit\EnumTestTrait; +use WordPress\AiClient\Tests\traits\EnumTestTrait; /** * @covers \WordPress\AiClient\Messages\Enums\MessagePartTypeEnum diff --git a/tests/unit/Messages/Enums/MessageRoleEnumTest.php b/tests/unit/Messages/Enums/MessageRoleEnumTest.php index 9e354325..61418b44 100644 --- a/tests/unit/Messages/Enums/MessageRoleEnumTest.php +++ b/tests/unit/Messages/Enums/MessageRoleEnumTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; -use WordPress\AiClient\Tests\unit\EnumTestTrait; +use WordPress\AiClient\Tests\traits\EnumTestTrait; /** * @covers \WordPress\AiClient\Messages\Enums\MessageRoleEnum diff --git a/tests/unit/Messages/Enums/ModalityEnumTest.php b/tests/unit/Messages/Enums/ModalityEnumTest.php index cea4e5f0..bac0a452 100644 --- a/tests/unit/Messages/Enums/ModalityEnumTest.php +++ b/tests/unit/Messages/Enums/ModalityEnumTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Messages\Enums\ModalityEnum; -use WordPress\AiClient\Tests\unit\EnumTestTrait; +use WordPress\AiClient\Tests\traits\EnumTestTrait; /** * @covers \WordPress\AiClient\Messages\Enums\ModalityEnum diff --git a/tests/unit/Operations/Enums/OperationStateEnumTest.php b/tests/unit/Operations/Enums/OperationStateEnumTest.php index fd7f2962..3de8cadc 100644 --- a/tests/unit/Operations/Enums/OperationStateEnumTest.php +++ b/tests/unit/Operations/Enums/OperationStateEnumTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Operations\Enums\OperationStateEnum; -use WordPress\AiClient\Tests\unit\EnumTestTrait; +use WordPress\AiClient\Tests\traits\EnumTestTrait; /** * @covers \WordPress\AiClient\Operations\Enums\OperationStateEnum diff --git a/tests/unit/Providers/Enums/ProviderTypeEnumTest.php b/tests/unit/Providers/Enums/ProviderTypeEnumTest.php index e6582317..905b579c 100644 --- a/tests/unit/Providers/Enums/ProviderTypeEnumTest.php +++ b/tests/unit/Providers/Enums/ProviderTypeEnumTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Providers\Enums\ProviderTypeEnum; -use WordPress\AiClient\Tests\unit\EnumTestTrait; +use WordPress\AiClient\Tests\traits\EnumTestTrait; /** * @covers \WordPress\AiClient\Providers\Enums\ProviderTypeEnum diff --git a/tests/unit/Providers/Enums/ToolTypeEnumTest.php b/tests/unit/Providers/Enums/ToolTypeEnumTest.php index ddd1a19e..10d545b3 100644 --- a/tests/unit/Providers/Enums/ToolTypeEnumTest.php +++ b/tests/unit/Providers/Enums/ToolTypeEnumTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Providers\Enums\ToolTypeEnum; -use WordPress\AiClient\Tests\unit\EnumTestTrait; +use WordPress\AiClient\Tests\traits\EnumTestTrait; /** * @covers \WordPress\AiClient\Providers\Enums\ToolTypeEnum diff --git a/tests/unit/Providers/Models/Enums/CapabilityEnumTest.php b/tests/unit/Providers/Models/Enums/CapabilityEnumTest.php index fa1e0b0d..c0f006da 100644 --- a/tests/unit/Providers/Models/Enums/CapabilityEnumTest.php +++ b/tests/unit/Providers/Models/Enums/CapabilityEnumTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; -use WordPress\AiClient\Tests\unit\EnumTestTrait; +use WordPress\AiClient\Tests\traits\EnumTestTrait; /** * @covers \WordPress\AiClient\Providers\Models\Enums\CapabilityEnum diff --git a/tests/unit/Providers/Models/Enums/OptionEnumTest.php b/tests/unit/Providers/Models/Enums/OptionEnumTest.php index 68698982..06d40d73 100644 --- a/tests/unit/Providers/Models/Enums/OptionEnumTest.php +++ b/tests/unit/Providers/Models/Enums/OptionEnumTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Providers\Models\Enums\OptionEnum; -use WordPress\AiClient\Tests\unit\EnumTestTrait; +use WordPress\AiClient\Tests\traits\EnumTestTrait; /** * @covers \WordPress\AiClient\Providers\Models\Enums\OptionEnum diff --git a/tests/unit/Results/Enums/FinishReasonEnumTest.php b/tests/unit/Results/Enums/FinishReasonEnumTest.php index 48832032..979574bc 100644 --- a/tests/unit/Results/Enums/FinishReasonEnumTest.php +++ b/tests/unit/Results/Enums/FinishReasonEnumTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Results\Enums\FinishReasonEnum; -use WordPress\AiClient\Tests\unit\EnumTestTrait; +use WordPress\AiClient\Tests\traits\EnumTestTrait; /** * @covers \WordPress\AiClient\Results\Enums\FinishReasonEnum From 03f66ffdc55d45419e287593a59caefed7e02a99 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 09:49:03 -0600 Subject: [PATCH 04/36] test: adds serialization tests --- tests/traits/JsonSerializationTestTrait.php | 93 +++++++++ tests/unit/Files/DTO/FileTest.php | 127 ++++++++++++ tests/unit/Messages/DTO/MessagePartTest.php | 132 ++++++++++++ tests/unit/Messages/DTO/MessageTest.php | 92 +++++++++ tests/unit/Messages/DTO/ModelMessageTest.php | 100 ++++++++- tests/unit/Messages/DTO/SystemMessageTest.php | 87 ++++++++ tests/unit/Messages/DTO/UserMessageTest.php | 87 ++++++++ .../DTO/GenerativeAiOperationTest.php | 189 ++++++++++++++++++ tests/unit/Results/DTO/CandidateTest.php | 108 ++++++++++ .../Results/DTO/GenerativeAiResultTest.php | 161 +++++++++++++++ tests/unit/Results/DTO/TokenUsageTest.php | 76 +++++++ tests/unit/Tools/DTO/FunctionCallTest.php | 121 +++++++++++ .../Tools/DTO/FunctionDeclarationTest.php | 138 +++++++++++++ tests/unit/Tools/DTO/FunctionResponseTest.php | 68 +++++++ tests/unit/Tools/DTO/ToolTest.php | 161 +++++++++++++++ tests/unit/Tools/DTO/WebSearchTest.php | 156 +++++++++++++++ 16 files changed, 1893 insertions(+), 3 deletions(-) create mode 100644 tests/traits/JsonSerializationTestTrait.php diff --git a/tests/traits/JsonSerializationTestTrait.php b/tests/traits/JsonSerializationTestTrait.php new file mode 100644 index 00000000..de3d83c8 --- /dev/null +++ b/tests/traits/JsonSerializationTestTrait.php @@ -0,0 +1,93 @@ +assertInstanceOf( + \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, + $object, + 'Object should implement WithJsonSerialization interface' + ); + $this->assertInstanceOf( + \JsonSerializable::class, + $object, + 'Object should implement JsonSerializable interface' + ); + } + + /** + * Asserts that jsonSerialize returns a valid array. + * + * @param object $object The object to test. + * @return array The serialized data. + */ + protected function assertJsonSerializeReturnsArray($object): array + { + $json = $object->jsonSerialize(); + $this->assertIsArray($json, 'jsonSerialize() should return an array'); + return $json; + } + + /** + * Asserts round-trip JSON serialization works correctly. + * + * @param object $original The original object. + * @param callable $assertCallback Callback to assert equality between original and restored. + * @return void + */ + protected function assertJsonRoundTrip($original, callable $assertCallback): void + { + $json = $original->jsonSerialize(); + $className = get_class($original); + $restored = $className::fromJson($json); + + $this->assertInstanceOf($className, $restored, 'fromJson() should return instance of ' . $className); + $assertCallback($original, $restored); + } + + /** + * Asserts that specific keys exist in serialized JSON. + * + * @param array $json The serialized JSON array. + * @param array $expectedKeys The keys that should exist. + * @return void + */ + protected function assertJsonHasKeys(array $json, array $expectedKeys): void + { + foreach ($expectedKeys as $key) { + $this->assertArrayHasKey($key, $json, "JSON should contain key: {$key}"); + } + } + + /** + * Asserts that specific keys do not exist in serialized JSON. + * + * @param array $json The serialized JSON array. + * @param array $unexpectedKeys The keys that should not exist. + * @return void + */ + protected function assertJsonNotHasKeys(array $json, array $unexpectedKeys): void + { + foreach ($unexpectedKeys as $key) { + $this->assertArrayNotHasKey($key, $json, "JSON should not contain key: {$key}"); + } + } +} \ No newline at end of file diff --git a/tests/unit/Files/DTO/FileTest.php b/tests/unit/Files/DTO/FileTest.php index 15ee2ae4..e9e5c128 100644 --- a/tests/unit/Files/DTO/FileTest.php +++ b/tests/unit/Files/DTO/FileTest.php @@ -267,4 +267,131 @@ public function testUrlWithUnknownExtension(): void new File('https://example.com/file.unknown'); } + + /** + * Tests JSON serialization for remote file. + * + * @return void + */ + public function testJsonSerializeRemoteFile(): void + { + $file = new File('https://example.com/image.jpg', 'image/jpeg'); + $json = $file->jsonSerialize(); + + $this->assertIsArray($json); + $this->assertEquals(\WordPress\AiClient\Files\Enums\FileTypeEnum::remote()->value, $json['fileType']); + $this->assertEquals('image/jpeg', $json['mimeType']); + $this->assertEquals('https://example.com/image.jpg', $json['url']); + $this->assertArrayNotHasKey('base64Data', $json); + } + + /** + * Tests JSON serialization for inline file. + * + * @return void + */ + public function testJsonSerializeInlineFile(): void + { + $base64Data = 'SGVsbG8gV29ybGQ='; + $dataUri = 'data:text/plain;base64,' . $base64Data; + $file = new File($dataUri); + $json = $file->jsonSerialize(); + + $this->assertIsArray($json); + $this->assertEquals(\WordPress\AiClient\Files\Enums\FileTypeEnum::inline()->value, $json['fileType']); + $this->assertEquals('text/plain', $json['mimeType']); + $this->assertEquals($base64Data, $json['base64Data']); + $this->assertArrayNotHasKey('url', $json); + } + + /** + * Tests fromJson for remote file. + * + * @return void + */ + public function testFromJsonRemoteFile(): void + { + $json = [ + 'fileType' => \WordPress\AiClient\Files\Enums\FileTypeEnum::remote()->value, + 'mimeType' => 'image/png', + 'url' => 'https://example.com/test.png' + ]; + + $file = File::fromJson($json); + + $this->assertInstanceOf(File::class, $file); + $this->assertTrue($file->getFileType()->isRemote()); + $this->assertEquals('image/png', $file->getMimeType()); + $this->assertEquals('https://example.com/test.png', $file->getUrl()); + $this->assertNull($file->getBase64Data()); + } + + /** + * Tests fromJson for inline file. + * + * @return void + */ + public function testFromJsonInlineFile(): void + { + $base64Data = 'SGVsbG8gV29ybGQ='; + $json = [ + 'fileType' => \WordPress\AiClient\Files\Enums\FileTypeEnum::inline()->value, + 'mimeType' => 'text/plain', + 'base64Data' => $base64Data + ]; + + $file = File::fromJson($json); + + $this->assertInstanceOf(File::class, $file); + $this->assertTrue($file->getFileType()->isInline()); + $this->assertEquals('text/plain', $file->getMimeType()); + $this->assertEquals($base64Data, $file->getBase64Data()); + $this->assertNull($file->getUrl()); + } + + /** + * Tests round-trip JSON serialization. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + // Test remote file + $remoteFile = new File('https://example.com/doc.pdf', 'application/pdf'); + $remoteJson = $remoteFile->jsonSerialize(); + $restoredRemote = File::fromJson($remoteJson); + + $this->assertEquals($remoteFile->getFileType()->value, $restoredRemote->getFileType()->value); + $this->assertEquals($remoteFile->getMimeType(), $restoredRemote->getMimeType()); + $this->assertEquals($remoteFile->getUrl(), $restoredRemote->getUrl()); + + // Test inline file + $dataUri = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + $inlineFile = new File($dataUri); + $inlineJson = $inlineFile->jsonSerialize(); + $restoredInline = File::fromJson($inlineJson); + + $this->assertEquals($inlineFile->getFileType()->value, $restoredInline->getFileType()->value); + $this->assertEquals($inlineFile->getMimeType(), $restoredInline->getMimeType()); + $this->assertEquals($inlineFile->getBase64Data(), $restoredInline->getBase64Data()); + } + + /** + * Tests File implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $file = new File('https://example.com/test.jpg'); + + $this->assertInstanceOf( + \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, + $file + ); + $this->assertInstanceOf( + \JsonSerializable::class, + $file + ); + } } \ No newline at end of file diff --git a/tests/unit/Messages/DTO/MessagePartTest.php b/tests/unit/Messages/DTO/MessagePartTest.php index 0bdbccdf..d49fcdea 100644 --- a/tests/unit/Messages/DTO/MessagePartTest.php +++ b/tests/unit/Messages/DTO/MessagePartTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Files\DTO\File; +use WordPress\AiClient\Files\Enums\FileTypeEnum; use WordPress\AiClient\Messages\DTO\MessagePart; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Tools\DTO\FunctionCall; @@ -230,4 +231,135 @@ public function testWithUnicodeText(): void $this->assertEquals($unicodeText, $part->getText()); } + + /** + * Tests JSON serialization with text content. + * + * @return void + */ + public function testJsonSerializeWithText(): void + { + $part = new MessagePart('Hello, world!'); + $json = $part->jsonSerialize(); + + $this->assertIsArray($json); + $this->assertArrayHasKey('type', $json); + $this->assertArrayHasKey('text', $json); + $this->assertEquals(MessagePartTypeEnum::text()->value, $json['type']); + $this->assertEquals('Hello, world!', $json['text']); + + // Ensure other fields are not present + $this->assertArrayNotHasKey('file', $json); + $this->assertArrayNotHasKey('functionCall', $json); + $this->assertArrayNotHasKey('functionResponse', $json); + } + + /** + * Tests JSON serialization with file content. + * + * @return void + */ + public function testJsonSerializeWithFile(): void + { + $file = new File('https://example.com/image.jpg', 'image/jpeg'); + $part = new MessagePart($file); + $json = $part->jsonSerialize(); + + $this->assertIsArray($json); + $this->assertArrayHasKey('type', $json); + $this->assertArrayHasKey('file', $json); + $this->assertEquals(MessagePartTypeEnum::file()->value, $json['type']); + $this->assertIsArray($json['file']); + } + + /** + * Tests fromJson with text content. + * + * @return void + */ + public function testFromJsonWithText(): void + { + $json = [ + 'type' => MessagePartTypeEnum::text()->value, + 'text' => 'Test message' + ]; + + $part = MessagePart::fromJson($json); + + $this->assertEquals(MessagePartTypeEnum::text(), $part->getType()); + $this->assertEquals('Test message', $part->getText()); + } + + /** + * Tests fromJson with file content. + * + * @return void + */ + public function testFromJsonWithFile(): void + { + $json = [ + 'type' => MessagePartTypeEnum::file()->value, + 'file' => [ + 'fileType' => FileTypeEnum::remote()->value, + 'mimeType' => 'image/jpeg', + 'url' => 'https://example.com/image.jpg' + ] + ]; + + $part = MessagePart::fromJson($json); + + $this->assertEquals(MessagePartTypeEnum::file(), $part->getType()); + $this->assertInstanceOf(File::class, $part->getFile()); + $this->assertEquals('https://example.com/image.jpg', $part->getFile()->getUrl()); + } + + /** + * Tests round-trip JSON serialization with different content types. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + // Test with text + $textPart = new MessagePart('Test text'); + $textJson = $textPart->jsonSerialize(); + $restoredText = MessagePart::fromJson($textJson); + $this->assertEquals($textPart->getText(), $restoredText->getText()); + + // Test with file + $file = new File('https://example.com/doc.pdf', 'application/pdf'); + $filePart = new MessagePart($file); + $fileJson = $filePart->jsonSerialize(); + $restoredFile = MessagePart::fromJson($fileJson); + $this->assertEquals($file->getUrl(), $restoredFile->getFile()->getUrl()); + $this->assertEquals($file->getMimeType(), $restoredFile->getFile()->getMimeType()); + + // Test with function call + $functionCall = new FunctionCall('id_123', 'getData', ['key' => 'value']); + $funcPart = new MessagePart($functionCall); + $funcJson = $funcPart->jsonSerialize(); + $restoredFunc = MessagePart::fromJson($funcJson); + $this->assertEquals($functionCall->getId(), $restoredFunc->getFunctionCall()->getId()); + $this->assertEquals($functionCall->getName(), $restoredFunc->getFunctionCall()->getName()); + $this->assertEquals($functionCall->getArgs(), $restoredFunc->getFunctionCall()->getArgs()); + } + + /** + * Tests MessagePart implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $part = new MessagePart('test'); + + $this->assertInstanceOf( + \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, + $part + ); + $this->assertInstanceOf( + \JsonSerializable::class, + $part + ); + } } \ No newline at end of file diff --git a/tests/unit/Messages/DTO/MessageTest.php b/tests/unit/Messages/DTO/MessageTest.php index 763696a7..a7b178c7 100644 --- a/tests/unit/Messages/DTO/MessageTest.php +++ b/tests/unit/Messages/DTO/MessageTest.php @@ -8,6 +8,7 @@ use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\DTO\Message; use WordPress\AiClient\Messages\DTO\MessagePart; +use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; use WordPress\AiClient\Tools\DTO\FunctionCall; use WordPress\AiClient\Tools\DTO\FunctionResponse; @@ -227,4 +228,95 @@ public function testModelMessageWithFunctionResponse(): void $this->assertTrue($message->getRole()->isModel()); $this->assertNotNull($message->getParts()[0]->getFunctionResponse()); } + + /** + * Tests JSON serialization. + * + * @return void + */ + public function testJsonSerialize(): void + { + $role = MessageRoleEnum::user(); + $parts = [ + new MessagePart('Hello, world!'), + new MessagePart('How are you?') + ]; + $message = new Message($role, $parts); + $json = $message->jsonSerialize(); + + $this->assertIsArray($json); + $this->assertEquals($role->value, $json['role']); + $this->assertIsArray($json['parts']); + $this->assertCount(2, $json['parts']); + $this->assertEquals('Hello, world!', $json['parts'][0]['text']); + $this->assertEquals('How are you?', $json['parts'][1]['text']); + } + + /** + * Tests fromJson method. + * + * @return void + */ + public function testFromJson(): void + { + $json = [ + 'role' => MessageRoleEnum::system()->value, + 'parts' => [ + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'You are a helpful assistant.'] + ] + ]; + + $message = Message::fromJson($json); + + $this->assertInstanceOf(Message::class, $message); + $this->assertEquals(MessageRoleEnum::system(), $message->getRole()); + $this->assertCount(1, $message->getParts()); + $this->assertEquals('You are a helpful assistant.', $message->getParts()[0]->getText()); + } + + /** + * Tests round-trip JSON serialization. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + $original = new Message( + MessageRoleEnum::model(), + [ + new MessagePart('Here is the result:'), + new MessagePart(new File('https://example.com/result.png', 'image/png')) + ] + ); + + $json = $original->jsonSerialize(); + $restored = Message::fromJson($json); + + $this->assertEquals($original->getRole()->value, $restored->getRole()->value); + $this->assertCount(count($original->getParts()), $restored->getParts()); + $this->assertEquals($original->getParts()[0]->getText(), $restored->getParts()[0]->getText()); + $this->assertEquals( + $original->getParts()[1]->getFile()->getUrl(), + $restored->getParts()[1]->getFile()->getUrl() + ); + } + + /** + * Tests Message implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $message = new Message(MessageRoleEnum::user(), [new MessagePart('test')]); + + $this->assertInstanceOf( + \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, + $message + ); + $this->assertInstanceOf( + \JsonSerializable::class, + $message + ); + } } \ No newline at end of file diff --git a/tests/unit/Messages/DTO/ModelMessageTest.php b/tests/unit/Messages/DTO/ModelMessageTest.php index 9536b0e7..3fafb01d 100644 --- a/tests/unit/Messages/DTO/ModelMessageTest.php +++ b/tests/unit/Messages/DTO/ModelMessageTest.php @@ -5,15 +5,22 @@ namespace WordPress\AiClient\Tests\unit\Messages\DTO; use PHPUnit\Framework\TestCase; +use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\DTO\MessagePart; use WordPress\AiClient\Messages\DTO\ModelMessage; +use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; +use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tools\DTO\FunctionCall; +use WordPress\AiClient\Tools\DTO\FunctionResponse; /** * @covers \WordPress\AiClient\Messages\DTO\ModelMessage */ class ModelMessageTest extends TestCase { + use JsonSerializationTestTrait; + /** * Tests creating ModelMessage automatically sets MODEL role. * @@ -83,9 +90,9 @@ public function testInheritsFromMessage(): void */ public function testWithVariousContentTypes(): void { - $file = new \WordPress\AiClient\Files\DTO\File('https://example.com/image.jpg', 'image/jpeg'); - $functionCall = new \WordPress\AiClient\Tools\DTO\FunctionCall('func_123', 'search', ['q' => 'test']); - $functionResponse = new \WordPress\AiClient\Tools\DTO\FunctionResponse('func_123', 'search', ['results' => []]); + $file = new File('https://example.com/image.jpg', 'image/jpeg'); + $functionCall = new FunctionCall('func_123', 'search', ['q' => 'test']); + $functionResponse = new FunctionResponse('func_123', 'search', ['results' => []]); $parts = [ new MessagePart('I found the following:'), @@ -114,4 +121,91 @@ public function testJsonSchemaInheritance(): void $this->assertEquals($parentSchema, $schema); } + + /** + * Tests JSON serialization. + * + * @return void + */ + public function testJsonSerialize(): void + { + $message = new ModelMessage([ + new MessagePart('I can help you with that.'), + new MessagePart('Here is the solution:') + ]); + + $json = $this->assertJsonSerializeReturnsArray($message); + + $this->assertJsonHasKeys($json, ['role', 'parts']); + $this->assertEquals(MessageRoleEnum::model()->value, $json['role']); + $this->assertCount(2, $json['parts']); + $this->assertEquals('I can help you with that.', $json['parts'][0]['text']); + $this->assertEquals('Here is the solution:', $json['parts'][1]['text']); + } + + /** + * Tests fromJson method. + * + * @return void + */ + public function testFromJson(): void + { + $json = [ + 'role' => MessageRoleEnum::model()->value, + 'parts' => [ + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'Model response 1'], + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'Model response 2'] + ] + ]; + + $message = ModelMessage::fromJson($json); + + $this->assertInstanceOf(ModelMessage::class, $message); + $this->assertEquals(MessageRoleEnum::model(), $message->getRole()); + $this->assertCount(2, $message->getParts()); + $this->assertEquals('Model response 1', $message->getParts()[0]->getText()); + $this->assertEquals('Model response 2', $message->getParts()[1]->getText()); + } + + /** + * Tests round-trip JSON serialization with function call. + * + * @return void + */ + public function testJsonRoundTripWithFunctionCall(): void + { + $this->assertJsonRoundTrip( + new ModelMessage([ + new MessagePart('I\'ll search for that information.'), + new MessagePart(new FunctionCall('search_123', 'webSearch', ['query' => 'PHP 8 features'])) + ]), + function ($original, $restored) { + $this->assertEquals($original->getRole()->value, $restored->getRole()->value); + $this->assertCount(count($original->getParts()), $restored->getParts()); + $this->assertEquals( + $original->getParts()[0]->getText(), + $restored->getParts()[0]->getText() + ); + $this->assertEquals( + $original->getParts()[1]->getFunctionCall()->getId(), + $restored->getParts()[1]->getFunctionCall()->getId() + ); + $this->assertEquals( + $original->getParts()[1]->getFunctionCall()->getName(), + $restored->getParts()[1]->getFunctionCall()->getName() + ); + } + ); + } + + /** + * Tests ModelMessage implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $message = new ModelMessage([new MessagePart('test')]); + $this->assertImplementsJsonSerialization($message); + } } \ No newline at end of file diff --git a/tests/unit/Messages/DTO/SystemMessageTest.php b/tests/unit/Messages/DTO/SystemMessageTest.php index 5d80ef2b..5e3ce8c3 100644 --- a/tests/unit/Messages/DTO/SystemMessageTest.php +++ b/tests/unit/Messages/DTO/SystemMessageTest.php @@ -7,13 +7,17 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Messages\DTO\MessagePart; use WordPress\AiClient\Messages\DTO\SystemMessage; +use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; +use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; /** * @covers \WordPress\AiClient\Messages\DTO\SystemMessage */ class SystemMessageTest extends TestCase { + use JsonSerializationTestTrait; + /** * Tests creating SystemMessage automatically sets SYSTEM role. * @@ -163,4 +167,87 @@ public function testPreservesPartOrder(): void $this->assertEquals('Third instruction', $retrievedParts[2]->getText()); $this->assertEquals('Fourth instruction', $retrievedParts[3]->getText()); } + + /** + * Tests JSON serialization. + * + * @return void + */ + public function testJsonSerialize(): void + { + $message = new SystemMessage([ + new MessagePart('You are a helpful assistant.'), + new MessagePart('Always be respectful and accurate.') + ]); + + $json = $this->assertJsonSerializeReturnsArray($message); + + $this->assertJsonHasKeys($json, ['role', 'parts']); + $this->assertEquals(MessageRoleEnum::system()->value, $json['role']); + $this->assertCount(2, $json['parts']); + $this->assertEquals('You are a helpful assistant.', $json['parts'][0]['text']); + $this->assertEquals('Always be respectful and accurate.', $json['parts'][1]['text']); + } + + /** + * Tests fromJson method. + * + * @return void + */ + public function testFromJson(): void + { + $json = [ + 'role' => MessageRoleEnum::system()->value, + 'parts' => [ + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'System instruction 1'], + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'System instruction 2'] + ] + ]; + + $message = SystemMessage::fromJson($json); + + $this->assertInstanceOf(SystemMessage::class, $message); + $this->assertEquals(MessageRoleEnum::system(), $message->getRole()); + $this->assertCount(2, $message->getParts()); + $this->assertEquals('System instruction 1', $message->getParts()[0]->getText()); + $this->assertEquals('System instruction 2', $message->getParts()[1]->getText()); + } + + /** + * Tests round-trip JSON serialization. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + $this->assertJsonRoundTrip( + new SystemMessage([ + new MessagePart('You are an expert in PHP.'), + new MessagePart('Follow best practices.') + ]), + function ($original, $restored) { + $this->assertEquals($original->getRole()->value, $restored->getRole()->value); + $this->assertCount(count($original->getParts()), $restored->getParts()); + $this->assertEquals( + $original->getParts()[0]->getText(), + $restored->getParts()[0]->getText() + ); + $this->assertEquals( + $original->getParts()[1]->getText(), + $restored->getParts()[1]->getText() + ); + } + ); + } + + /** + * Tests SystemMessage implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $message = new SystemMessage([new MessagePart('test')]); + $this->assertImplementsJsonSerialization($message); + } } \ No newline at end of file diff --git a/tests/unit/Messages/DTO/UserMessageTest.php b/tests/unit/Messages/DTO/UserMessageTest.php index 2249d58c..ab56e94e 100644 --- a/tests/unit/Messages/DTO/UserMessageTest.php +++ b/tests/unit/Messages/DTO/UserMessageTest.php @@ -8,13 +8,17 @@ use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\DTO\MessagePart; use WordPress\AiClient\Messages\DTO\UserMessage; +use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; +use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; /** * @covers \WordPress\AiClient\Messages\DTO\UserMessage */ class UserMessageTest extends TestCase { + use JsonSerializationTestTrait; + /** * Tests creating UserMessage automatically sets USER role. * @@ -223,4 +227,87 @@ public function testWithMultipleFiles(): void $this->assertInstanceOf(File::class, $message->getParts()[2]->getFile()); $this->assertInstanceOf(File::class, $message->getParts()[4]->getFile()); } + + /** + * Tests JSON serialization. + * + * @return void + */ + public function testJsonSerialize(): void + { + $message = new UserMessage([ + new MessagePart('Hello, I need help'), + new MessagePart('Can you assist?') + ]); + + $json = $this->assertJsonSerializeReturnsArray($message); + + $this->assertJsonHasKeys($json, ['role', 'parts']); + $this->assertEquals(MessageRoleEnum::user()->value, $json['role']); + $this->assertCount(2, $json['parts']); + $this->assertEquals('Hello, I need help', $json['parts'][0]['text']); + $this->assertEquals('Can you assist?', $json['parts'][1]['text']); + } + + /** + * Tests fromJson method. + * + * @return void + */ + public function testFromJson(): void + { + $json = [ + 'role' => MessageRoleEnum::user()->value, + 'parts' => [ + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'Question 1'], + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'Question 2'] + ] + ]; + + $message = UserMessage::fromJson($json); + + $this->assertInstanceOf(UserMessage::class, $message); + $this->assertEquals(MessageRoleEnum::user(), $message->getRole()); + $this->assertCount(2, $message->getParts()); + $this->assertEquals('Question 1', $message->getParts()[0]->getText()); + $this->assertEquals('Question 2', $message->getParts()[1]->getText()); + } + + /** + * Tests round-trip JSON serialization. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + $this->assertJsonRoundTrip( + new UserMessage([ + new MessagePart('Test message'), + new MessagePart(new File('https://example.com/image.jpg', 'image/jpeg')) + ]), + function ($original, $restored) { + $this->assertEquals($original->getRole()->value, $restored->getRole()->value); + $this->assertCount(count($original->getParts()), $restored->getParts()); + $this->assertEquals( + $original->getParts()[0]->getText(), + $restored->getParts()[0]->getText() + ); + $this->assertEquals( + $original->getParts()[1]->getFile()->getUrl(), + $restored->getParts()[1]->getFile()->getUrl() + ); + } + ); + } + + /** + * Tests UserMessage implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $message = new UserMessage([new MessagePart('test')]); + $this->assertImplementsJsonSerialization($message); + } } \ No newline at end of file diff --git a/tests/unit/Operations/DTO/GenerativeAiOperationTest.php b/tests/unit/Operations/DTO/GenerativeAiOperationTest.php index be2d7970..2fdd9680 100644 --- a/tests/unit/Operations/DTO/GenerativeAiOperationTest.php +++ b/tests/unit/Operations/DTO/GenerativeAiOperationTest.php @@ -15,12 +15,14 @@ use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Results\DTO\TokenUsage; use WordPress\AiClient\Results\Enums\FinishReasonEnum; +use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; /** * @covers \WordPress\AiClient\Operations\DTO\GenerativeAiOperation */ class GenerativeAiOperationTest extends TestCase { + use JsonSerializationTestTrait; /** * Tests creating operation in starting state. * @@ -288,4 +290,191 @@ public function testWithEmptyStringId(): void $this->assertEquals('', $operation->getId()); } + + /** + * Tests JSON serialization for operation in starting state. + * + * @return void + */ + public function testJsonSerializeStartingState(): void + { + $operation = new GenerativeAiOperation( + 'op_start_123', + OperationStateEnum::starting() + ); + + $json = $this->assertJsonSerializeReturnsArray($operation); + + $this->assertJsonHasKeys($json, ['id', 'state']); + $this->assertJsonNotHasKeys($json, ['result']); + $this->assertEquals('op_start_123', $json['id']); + $this->assertEquals(OperationStateEnum::starting()->value, $json['state']); + } + + /** + * Tests JSON serialization for operation in succeeded state. + * + * @return void + */ + public function testJsonSerializeSucceededState(): void + { + $modelMessage = new ModelMessage([ + new MessagePart('Success response') + ]); + $candidate = new Candidate( + $modelMessage, + FinishReasonEnum::stop(), + 50 + ); + $tokenUsage = new TokenUsage(15, 50, 65); + $result = new GenerativeAiResult( + 'result_success', + [$candidate], + $tokenUsage + ); + + $operation = new GenerativeAiOperation( + 'op_success_456', + OperationStateEnum::succeeded(), + $result + ); + + $json = $this->assertJsonSerializeReturnsArray($operation); + + $this->assertJsonHasKeys($json, ['id', 'state', 'result']); + $this->assertEquals('op_success_456', $json['id']); + $this->assertEquals(OperationStateEnum::succeeded()->value, $json['state']); + $this->assertIsArray($json['result']); + $this->assertEquals('result_success', $json['result']['id']); + } + + /** + * Tests fromJson method for starting state. + * + * @return void + */ + public function testFromJsonStartingState(): void + { + $json = [ + 'id' => 'op_from_json_start', + 'state' => OperationStateEnum::starting()->value + ]; + + $operation = GenerativeAiOperation::fromJson($json); + + $this->assertInstanceOf(GenerativeAiOperation::class, $operation); + $this->assertEquals('op_from_json_start', $operation->getId()); + $this->assertEquals(OperationStateEnum::starting(), $operation->getState()); + $this->assertNull($operation->getResult()); + } + + /** + * Tests fromJson method for succeeded state with result. + * + * @return void + */ + public function testFromJsonSucceededState(): void + { + $json = [ + 'id' => 'op_from_json_success', + 'state' => OperationStateEnum::succeeded()->value, + 'result' => [ + 'id' => 'result_from_json', + 'candidates' => [ + [ + 'message' => [ + 'role' => MessageRoleEnum::model()->value, + 'parts' => [['type' => 'text', 'text' => 'Response text']] + ], + 'finishReason' => FinishReasonEnum::stop()->value, + 'tokenCount' => 30 + ] + ], + 'tokenUsage' => [ + 'inputTokens' => 10, + 'outputTokens' => 30, + 'totalTokens' => 40 + ] + ] + ]; + + $operation = GenerativeAiOperation::fromJson($json); + + $this->assertInstanceOf(GenerativeAiOperation::class, $operation); + $this->assertEquals('op_from_json_success', $operation->getId()); + $this->assertEquals(OperationStateEnum::succeeded(), $operation->getState()); + $this->assertNotNull($operation->getResult()); + $this->assertEquals('result_from_json', $operation->getResult()->getId()); + } + + /** + * Tests round-trip JSON serialization for processing state. + * + * @return void + */ + public function testJsonRoundTripProcessingState(): void + { + $this->assertJsonRoundTrip( + new GenerativeAiOperation( + 'op_roundtrip_process', + OperationStateEnum::processing() + ), + function ($original, $restored) { + $this->assertEquals($original->getId(), $restored->getId()); + $this->assertEquals($original->getState()->value, $restored->getState()->value); + $this->assertNull($restored->getResult()); + } + ); + } + + /** + * Tests round-trip JSON serialization for succeeded state. + * + * @return void + */ + public function testJsonRoundTripSucceededState(): void + { + $modelMessage = new ModelMessage([ + new MessagePart('Roundtrip test response') + ]); + $candidate = new Candidate( + $modelMessage, + FinishReasonEnum::stop(), + 25 + ); + $tokenUsage = new TokenUsage(5, 25, 30); + $result = new GenerativeAiResult( + 'result_roundtrip', + [$candidate], + $tokenUsage + ); + + $this->assertJsonRoundTrip( + new GenerativeAiOperation( + 'op_roundtrip_success', + OperationStateEnum::succeeded(), + $result + ), + function ($original, $restored) { + $this->assertEquals($original->getId(), $restored->getId()); + $this->assertEquals($original->getState()->value, $restored->getState()->value); + $this->assertNotNull($restored->getResult()); + $this->assertEquals($original->getResult()->getId(), $restored->getResult()->getId()); + } + ); + } + + /** + * Tests GenerativeAiOperation implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $operation = new GenerativeAiOperation( + 'op_test', + OperationStateEnum::starting() + ); + $this->assertImplementsJsonSerialization($operation); + } } \ No newline at end of file diff --git a/tests/unit/Results/DTO/CandidateTest.php b/tests/unit/Results/DTO/CandidateTest.php index 7a1fcd75..b6d38d62 100644 --- a/tests/unit/Results/DTO/CandidateTest.php +++ b/tests/unit/Results/DTO/CandidateTest.php @@ -10,9 +10,11 @@ use WordPress\AiClient\Messages\DTO\MessagePart; use WordPress\AiClient\Messages\DTO\ModelMessage; use WordPress\AiClient\Messages\DTO\UserMessage; +use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; use WordPress\AiClient\Results\DTO\Candidate; use WordPress\AiClient\Results\Enums\FinishReasonEnum; +use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; use WordPress\AiClient\Tools\DTO\FunctionCall; /** @@ -20,6 +22,7 @@ */ class CandidateTest extends TestCase { + use JsonSerializationTestTrait; /** * Tests creating candidate with basic properties. * @@ -329,4 +332,109 @@ public function testWithErrorFinishReason(): void $this->assertTrue($candidate->getFinishReason()->isError()); } + + /** + * Tests JSON serialization. + * + * @return void + */ + public function testJsonSerialize(): void + { + $message = new ModelMessage([ + new MessagePart('This is the AI response.'), + new MessagePart('It contains multiple parts.') + ]); + + $candidate = new Candidate( + $message, + FinishReasonEnum::stop(), + 45 + ); + + $json = $this->assertJsonSerializeReturnsArray($candidate); + + $this->assertJsonHasKeys($json, ['message', 'finishReason', 'tokenCount']); + $this->assertIsArray($json['message']); + $this->assertEquals(FinishReasonEnum::stop()->value, $json['finishReason']); + $this->assertEquals(45, $json['tokenCount']); + } + + /** + * Tests fromJson method. + * + * @return void + */ + public function testFromJson(): void + { + $json = [ + 'message' => [ + 'role' => MessageRoleEnum::model()->value, + 'parts' => [ + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'Response text 1'], + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'Response text 2'] + ] + ], + 'finishReason' => FinishReasonEnum::stop()->value, + 'tokenCount' => 75 + ]; + + $candidate = Candidate::fromJson($json); + + $this->assertInstanceOf(Candidate::class, $candidate); + $this->assertEquals(FinishReasonEnum::stop(), $candidate->getFinishReason()); + $this->assertEquals(75, $candidate->getTokenCount()); + $this->assertCount(2, $candidate->getMessage()->getParts()); + $this->assertEquals('Response text 1', $candidate->getMessage()->getParts()[0]->getText()); + $this->assertEquals('Response text 2', $candidate->getMessage()->getParts()[1]->getText()); + } + + /** + * Tests round-trip JSON serialization. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + $this->assertJsonRoundTrip( + new Candidate( + new ModelMessage([ + new MessagePart('Generated response'), + new MessagePart(new FunctionCall('call_123', 'search', ['q' => 'test'])) + ]), + FinishReasonEnum::toolCalls(), + 120 + ), + function ($original, $restored) { + $this->assertEquals($original->getFinishReason()->value, $restored->getFinishReason()->value); + $this->assertEquals($original->getTokenCount(), $restored->getTokenCount()); + $this->assertCount( + count($original->getMessage()->getParts()), + $restored->getMessage()->getParts() + ); + $this->assertEquals( + $original->getMessage()->getParts()[0]->getText(), + $restored->getMessage()->getParts()[0]->getText() + ); + $this->assertEquals( + $original->getMessage()->getParts()[1]->getFunctionCall()->getId(), + $restored->getMessage()->getParts()[1]->getFunctionCall()->getId() + ); + } + ); + } + + /** + * Tests Candidate implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $candidate = new Candidate( + new ModelMessage([new MessagePart('test')]), + FinishReasonEnum::stop(), + 10 + ); + $this->assertImplementsJsonSerialization($candidate); + } } \ No newline at end of file diff --git a/tests/unit/Results/DTO/GenerativeAiResultTest.php b/tests/unit/Results/DTO/GenerativeAiResultTest.php index 61d67580..c6ce4589 100644 --- a/tests/unit/Results/DTO/GenerativeAiResultTest.php +++ b/tests/unit/Results/DTO/GenerativeAiResultTest.php @@ -8,16 +8,21 @@ use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\DTO\MessagePart; use WordPress\AiClient\Messages\DTO\ModelMessage; +use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; +use WordPress\AiClient\Messages\Enums\MessageRoleEnum; use WordPress\AiClient\Results\DTO\Candidate; use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Results\DTO\TokenUsage; use WordPress\AiClient\Results\Enums\FinishReasonEnum; +use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tools\DTO\FunctionCall; /** * @covers \WordPress\AiClient\Results\DTO\GenerativeAiResult */ class GenerativeAiResultTest extends TestCase { + use JsonSerializationTestTrait; /** * Tests creating result with single candidate. * @@ -594,4 +599,160 @@ public function testHasMultipleCandidatesReturnsFalseForSingle(): void $this->assertFalse($result->hasMultipleCandidates()); $this->assertEquals(1, $result->getCandidateCount()); } + + /** + * Tests JSON serialization. + * + * @return void + */ + public function testJsonSerialize(): void + { + $message = new ModelMessage([ + new MessagePart('AI generated response'), + new MessagePart('with multiple parts') + ]); + $candidate = new Candidate($message, FinishReasonEnum::stop(), 15); + $tokenUsage = new TokenUsage(10, 15, 25); + $metadata = ['model' => 'test-model', 'version' => '1.0']; + + $result = new GenerativeAiResult( + 'result_json_123', + [$candidate], + $tokenUsage, + $metadata + ); + + $json = $this->assertJsonSerializeReturnsArray($result); + + $this->assertJsonHasKeys($json, ['id', 'candidates', 'tokenUsage', 'providerMetadata']); + $this->assertEquals('result_json_123', $json['id']); + $this->assertIsArray($json['candidates']); + $this->assertCount(1, $json['candidates']); + $this->assertIsArray($json['tokenUsage']); + $this->assertEquals($metadata, $json['providerMetadata']); + } + + /** + * Tests fromJson method. + * + * @return void + */ + public function testFromJson(): void + { + $json = [ + 'id' => 'result_from_json', + 'candidates' => [ + [ + 'message' => [ + 'role' => MessageRoleEnum::model()->value, + 'parts' => [ + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'First part'], + ['type' => MessagePartTypeEnum::text()->value, 'text' => 'Second part'] + ] + ], + 'finishReason' => FinishReasonEnum::stop()->value, + 'tokenCount' => 20 + ] + ], + 'tokenUsage' => [ + 'inputTokens' => 8, + 'outputTokens' => 20, + 'totalTokens' => 28 + ], + 'providerMetadata' => ['provider' => 'test'] + ]; + + $result = GenerativeAiResult::fromJson($json); + + $this->assertInstanceOf(GenerativeAiResult::class, $result); + $this->assertEquals('result_from_json', $result->getId()); + $this->assertCount(1, $result->getCandidates()); + $this->assertEquals(8, $result->getTokenUsage()->getInputTokens()); + $this->assertEquals(20, $result->getTokenUsage()->getOutputTokens()); + $this->assertEquals(28, $result->getTokenUsage()->getTotalTokens()); + $this->assertEquals(['provider' => 'test'], $result->getProviderMetadata()); + } + + /** + * Tests round-trip JSON serialization with multiple candidates. + * + * @return void + */ + public function testJsonRoundTripWithMultipleCandidates(): void + { + $candidates = []; + for ($i = 1; $i <= 2; $i++) { + $message = new ModelMessage([ + new MessagePart("Response $i"), + new MessagePart(new FunctionCall("call_$i", "func$i", ['arg' => $i])) + ]); + $candidates[] = new Candidate($message, FinishReasonEnum::toolCalls(), 25 * $i); + } + + $this->assertJsonRoundTrip( + new GenerativeAiResult( + 'result_roundtrip', + $candidates, + new TokenUsage(30, 75, 105), + ['test_meta' => true] + ), + function ($original, $restored) { + $this->assertEquals($original->getId(), $restored->getId()); + $this->assertCount(count($original->getCandidates()), $restored->getCandidates()); + $this->assertEquals($original->getTokenUsage()->getTotalTokens(), + $restored->getTokenUsage()->getTotalTokens()); + $this->assertEquals($original->getProviderMetadata(), $restored->getProviderMetadata()); + + // Check first candidate details + $originalFirst = $original->getCandidates()[0]; + $restoredFirst = $restored->getCandidates()[0]; + $this->assertEquals( + $originalFirst->getMessage()->getParts()[0]->getText(), + $restoredFirst->getMessage()->getParts()[0]->getText() + ); + $this->assertEquals( + $originalFirst->getMessage()->getParts()[1]->getFunctionCall()->getId(), + $restoredFirst->getMessage()->getParts()[1]->getFunctionCall()->getId() + ); + } + ); + } + + /** + * Tests JSON serialization without provider metadata. + * + * @return void + */ + public function testJsonSerializeWithoutProviderMetadata(): void + { + $message = new ModelMessage([new MessagePart('Simple response')]); + $candidate = new Candidate($message, FinishReasonEnum::stop(), 5); + $tokenUsage = new TokenUsage(3, 5, 8); + + $result = new GenerativeAiResult( + 'result_no_meta', + [$candidate], + $tokenUsage + ); + + $json = $this->assertJsonSerializeReturnsArray($result); + + $this->assertJsonHasKeys($json, ['id', 'candidates', 'tokenUsage', 'providerMetadata']); + $this->assertEquals([], $json['providerMetadata']); + } + + /** + * Tests GenerativeAiResult implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $message = new ModelMessage([new MessagePart('test')]); + $candidate = new Candidate($message, FinishReasonEnum::stop(), 1); + $tokenUsage = new TokenUsage(1, 1, 2); + + $result = new GenerativeAiResult('test', [$candidate], $tokenUsage); + $this->assertImplementsJsonSerialization($result); + } } \ No newline at end of file diff --git a/tests/unit/Results/DTO/TokenUsageTest.php b/tests/unit/Results/DTO/TokenUsageTest.php index af299c1f..a4f12ea5 100644 --- a/tests/unit/Results/DTO/TokenUsageTest.php +++ b/tests/unit/Results/DTO/TokenUsageTest.php @@ -208,6 +208,82 @@ public function testMultipleInstances(): void $this->assertEquals($usage1->getTotalTokens(), $usage3->getTotalTokens()); } + /** + * Tests JSON serialization. + * + * @return void + */ + public function testJsonSerialize(): void + { + $tokenUsage = new TokenUsage(100, 50, 150); + $json = $tokenUsage->jsonSerialize(); + + $this->assertIsArray($json); + $this->assertArrayHasKey('promptTokens', $json); + $this->assertArrayHasKey('completionTokens', $json); + $this->assertArrayHasKey('totalTokens', $json); + + $this->assertEquals(100, $json['promptTokens']); + $this->assertEquals(50, $json['completionTokens']); + $this->assertEquals(150, $json['totalTokens']); + } + + /** + * Tests fromJson method. + * + * @return void + */ + public function testFromJson(): void + { + $json = [ + 'promptTokens' => 100, + 'completionTokens' => 50, + 'totalTokens' => 150, + ]; + + $tokenUsage = TokenUsage::fromJson($json); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertEquals(100, $tokenUsage->getPromptTokens()); + $this->assertEquals(50, $tokenUsage->getCompletionTokens()); + $this->assertEquals(150, $tokenUsage->getTotalTokens()); + } + + /** + * Tests round-trip JSON serialization. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + $original = new TokenUsage(123, 456, 579); + $json = $original->jsonSerialize(); + $restored = TokenUsage::fromJson($json); + + $this->assertEquals($original->getPromptTokens(), $restored->getPromptTokens()); + $this->assertEquals($original->getCompletionTokens(), $restored->getCompletionTokens()); + $this->assertEquals($original->getTotalTokens(), $restored->getTotalTokens()); + } + + /** + * Tests TokenUsage implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $tokenUsage = new TokenUsage(10, 20, 30); + + $this->assertInstanceOf( + \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, + $tokenUsage + ); + $this->assertInstanceOf( + \JsonSerializable::class, + $tokenUsage + ); + } + /** * Tests TokenUsage with streaming response simulation. * diff --git a/tests/unit/Tools/DTO/FunctionCallTest.php b/tests/unit/Tools/DTO/FunctionCallTest.php index 0a387515..6342a4ca 100644 --- a/tests/unit/Tools/DTO/FunctionCallTest.php +++ b/tests/unit/Tools/DTO/FunctionCallTest.php @@ -161,4 +161,125 @@ public function testWithComplexArgs(): void $this->assertEquals($args, $functionCall->getArgs()); } + + /** + * Tests JSON serialization with all fields. + * + * @return void + */ + public function testJsonSerializeAllFields(): void + { + $functionCall = new FunctionCall('func_123', 'calculate', ['x' => 10, 'y' => 20]); + $json = $functionCall->jsonSerialize(); + + $this->assertIsArray($json); + $this->assertEquals('func_123', $json['id']); + $this->assertEquals('calculate', $json['name']); + $this->assertEquals(['x' => 10, 'y' => 20], $json['args']); + } + + /** + * Tests JSON serialization with only ID. + * + * @return void + */ + public function testJsonSerializeOnlyId(): void + { + $functionCall = new FunctionCall('func_456', null); + $json = $functionCall->jsonSerialize(); + + $this->assertIsArray($json); + $this->assertEquals('func_456', $json['id']); + $this->assertArrayNotHasKey('name', $json); + $this->assertArrayNotHasKey('args', $json); + } + + /** + * Tests JSON serialization with only name. + * + * @return void + */ + public function testJsonSerializeOnlyName(): void + { + $functionCall = new FunctionCall(null, 'search'); + $json = $functionCall->jsonSerialize(); + + $this->assertIsArray($json); + $this->assertEquals('search', $json['name']); + $this->assertArrayNotHasKey('id', $json); + $this->assertArrayNotHasKey('args', $json); + } + + /** + * Tests fromJson with all fields. + * + * @return void + */ + public function testFromJsonAllFields(): void + { + $json = [ + 'id' => 'func_789', + 'name' => 'process', + 'args' => ['input' => 'data', 'format' => 'json'] + ]; + + $functionCall = FunctionCall::fromJson($json); + + $this->assertInstanceOf(FunctionCall::class, $functionCall); + $this->assertEquals('func_789', $functionCall->getId()); + $this->assertEquals('process', $functionCall->getName()); + $this->assertEquals(['input' => 'data', 'format' => 'json'], $functionCall->getArgs()); + } + + /** + * Tests fromJson with minimal fields. + * + * @return void + */ + public function testFromJsonMinimalFields(): void + { + $json = ['name' => 'minimal']; + + $functionCall = FunctionCall::fromJson($json); + + $this->assertInstanceOf(FunctionCall::class, $functionCall); + $this->assertNull($functionCall->getId()); + $this->assertEquals('minimal', $functionCall->getName()); + $this->assertEquals([], $functionCall->getArgs()); + } + + /** + * Tests round-trip JSON serialization. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + $original = new FunctionCall('id_123', 'execute', ['param' => 'value', 'count' => 5]); + $json = $original->jsonSerialize(); + $restored = FunctionCall::fromJson($json); + + $this->assertEquals($original->getId(), $restored->getId()); + $this->assertEquals($original->getName(), $restored->getName()); + $this->assertEquals($original->getArgs(), $restored->getArgs()); + } + + /** + * Tests FunctionCall implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $functionCall = new FunctionCall('id', 'name'); + + $this->assertInstanceOf( + \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, + $functionCall + ); + $this->assertInstanceOf( + \JsonSerializable::class, + $functionCall + ); + } } \ No newline at end of file diff --git a/tests/unit/Tools/DTO/FunctionDeclarationTest.php b/tests/unit/Tools/DTO/FunctionDeclarationTest.php index b5a2f3d7..3d00241a 100644 --- a/tests/unit/Tools/DTO/FunctionDeclarationTest.php +++ b/tests/unit/Tools/DTO/FunctionDeclarationTest.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Tests\unit\Tools\DTO; use PHPUnit\Framework\TestCase; +use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; use WordPress\AiClient\Tools\DTO\FunctionDeclaration; /** @@ -12,6 +13,7 @@ */ class FunctionDeclarationTest extends TestCase { + use JsonSerializationTestTrait; /** * Tests creating FunctionDeclaration with all properties. * @@ -185,4 +187,140 @@ public function testWithOpenApiStyleSchema(): void $this->assertEquals($parameters, $declaration->getParameters()); } + + /** + * Tests JSON serialization with parameters. + * + * @return void + */ + public function testJsonSerializeWithParameters(): void + { + $declaration = new FunctionDeclaration( + 'searchWeb', + 'Searches the web for information', + ['type' => 'object', 'properties' => ['query' => ['type' => 'string']]] + ); + + $json = $this->assertJsonSerializeReturnsArray($declaration); + + $this->assertJsonHasKeys($json, ['name', 'description', 'parameters']); + $this->assertEquals('searchWeb', $json['name']); + $this->assertEquals('Searches the web for information', $json['description']); + $this->assertEquals(['type' => 'object', 'properties' => ['query' => ['type' => 'string']]], $json['parameters']); + } + + /** + * Tests JSON serialization without parameters. + * + * @return void + */ + public function testJsonSerializeWithoutParameters(): void + { + $declaration = new FunctionDeclaration( + 'getTimestamp', + 'Returns the current Unix timestamp' + ); + + $json = $this->assertJsonSerializeReturnsArray($declaration); + + $this->assertJsonHasKeys($json, ['name', 'description']); + $this->assertArrayNotHasKey('parameters', $json); + $this->assertEquals('getTimestamp', $json['name']); + $this->assertEquals('Returns the current Unix timestamp', $json['description']); + } + + /** + * Tests fromJson method with parameters. + * + * @return void + */ + public function testFromJsonWithParameters(): void + { + $json = [ + 'name' => 'calculateArea', + 'description' => 'Calculates the area of a rectangle', + 'parameters' => [ + 'type' => 'object', + 'properties' => [ + 'width' => ['type' => 'number'], + 'height' => ['type' => 'number'] + ], + 'required' => ['width', 'height'] + ] + ]; + + $declaration = FunctionDeclaration::fromJson($json); + + $this->assertInstanceOf(FunctionDeclaration::class, $declaration); + $this->assertEquals('calculateArea', $declaration->getName()); + $this->assertEquals('Calculates the area of a rectangle', $declaration->getDescription()); + $this->assertEquals($json['parameters'], $declaration->getParameters()); + } + + /** + * Tests fromJson method without parameters. + * + * @return void + */ + public function testFromJsonWithoutParameters(): void + { + $json = [ + 'name' => 'ping', + 'description' => 'Simple ping function' + ]; + + $declaration = FunctionDeclaration::fromJson($json); + + $this->assertInstanceOf(FunctionDeclaration::class, $declaration); + $this->assertEquals('ping', $declaration->getName()); + $this->assertEquals('Simple ping function', $declaration->getDescription()); + $this->assertNull($declaration->getParameters()); + } + + /** + * Tests round-trip JSON serialization. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + $this->assertJsonRoundTrip( + new FunctionDeclaration( + 'complexFunction', + 'A complex function with nested parameters', + [ + 'type' => 'object', + 'properties' => [ + 'user' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'age' => ['type' => 'integer', 'minimum' => 0] + ] + ], + 'options' => [ + 'type' => 'array', + 'items' => ['type' => 'string'] + ] + ] + ] + ), + function ($original, $restored) { + $this->assertEquals($original->getName(), $restored->getName()); + $this->assertEquals($original->getDescription(), $restored->getDescription()); + $this->assertEquals($original->getParameters(), $restored->getParameters()); + } + ); + } + + /** + * Tests FunctionDeclaration implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $declaration = new FunctionDeclaration('test', 'test function'); + $this->assertImplementsJsonSerialization($declaration); + } } \ No newline at end of file diff --git a/tests/unit/Tools/DTO/FunctionResponseTest.php b/tests/unit/Tools/DTO/FunctionResponseTest.php index 20180ab9..1f707c26 100644 --- a/tests/unit/Tools/DTO/FunctionResponseTest.php +++ b/tests/unit/Tools/DTO/FunctionResponseTest.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Tests\unit\Tools\DTO; use PHPUnit\Framework\TestCase; +use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; use WordPress\AiClient\Tools\DTO\FunctionResponse; /** @@ -12,6 +13,8 @@ */ class FunctionResponseTest extends TestCase { + use JsonSerializationTestTrait; + /** * Tests creating FunctionResponse with all properties. * @@ -180,4 +183,69 @@ public function testWithLargeResponseData(): void $this->assertEquals($largeData, $response->getResponse()); $this->assertCount(1000, $response->getResponse()); } + + /** + * Tests JSON serialization. + * + * @return void + */ + public function testJsonSerialize(): void + { + $response = new FunctionResponse('func_123', 'calculate', ['result' => 42]); + $json = $this->assertJsonSerializeReturnsArray($response); + + $this->assertJsonHasKeys($json, ['id', 'name', 'response']); + $this->assertEquals('func_123', $json['id']); + $this->assertEquals('calculate', $json['name']); + $this->assertEquals(['result' => 42], $json['response']); + } + + /** + * Tests fromJson method. + * + * @return void + */ + public function testFromJson(): void + { + $json = [ + 'id' => 'func_456', + 'name' => 'search', + 'response' => ['found' => true, 'count' => 5] + ]; + + $response = FunctionResponse::fromJson($json); + + $this->assertInstanceOf(FunctionResponse::class, $response); + $this->assertEquals('func_456', $response->getId()); + $this->assertEquals('search', $response->getName()); + $this->assertEquals(['found' => true, 'count' => 5], $response->getResponse()); + } + + /** + * Tests round-trip JSON serialization. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + $this->assertJsonRoundTrip( + new FunctionResponse('id_789', 'process', ['status' => 'complete']), + function ($original, $restored) { + $this->assertEquals($original->getId(), $restored->getId()); + $this->assertEquals($original->getName(), $restored->getName()); + $this->assertEquals($original->getResponse(), $restored->getResponse()); + } + ); + } + + /** + * Tests FunctionResponse implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $response = new FunctionResponse('id', 'name', 'result'); + $this->assertImplementsJsonSerialization($response); + } } \ No newline at end of file diff --git a/tests/unit/Tools/DTO/ToolTest.php b/tests/unit/Tools/DTO/ToolTest.php index 74058ade..911278db 100644 --- a/tests/unit/Tools/DTO/ToolTest.php +++ b/tests/unit/Tools/DTO/ToolTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Providers\Enums\ToolTypeEnum; +use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; use WordPress\AiClient\Tools\DTO\FunctionDeclaration; use WordPress\AiClient\Tools\DTO\Tool; use WordPress\AiClient\Tools\DTO\WebSearch; @@ -15,6 +16,7 @@ */ class ToolTest extends TestCase { + use JsonSerializationTestTrait; /** * Tests creating tool with function declarations. * @@ -295,4 +297,163 @@ public function testMultipleToolInstances(): void $this->assertNull($tool2->getFunctionDeclarations()); $this->assertNotNull($tool2->getWebSearch()); } + + /** + * Tests JSON serialization with function declarations. + * + * @return void + */ + public function testJsonSerializeWithFunctionDeclarations(): void + { + $functions = [ + new FunctionDeclaration('func1', 'First function', ['param1' => ['type' => 'string']]), + new FunctionDeclaration('func2', 'Second function') + ]; + + $tool = new Tool($functions); + $json = $this->assertJsonSerializeReturnsArray($tool); + + $this->assertJsonHasKeys($json, ['type', 'functionDeclarations']); + $this->assertEquals(ToolTypeEnum::functionDeclarations()->value, $json['type']); + $this->assertIsArray($json['functionDeclarations']); + $this->assertCount(2, $json['functionDeclarations']); + $this->assertEquals('func1', $json['functionDeclarations'][0]['name']); + $this->assertEquals('func2', $json['functionDeclarations'][1]['name']); + } + + /** + * Tests JSON serialization with web search. + * + * @return void + */ + public function testJsonSerializeWithWebSearch(): void + { + $webSearch = new WebSearch( + ['allowed1.com', 'allowed2.com'], + ['blocked1.com', 'blocked2.com'] + ); + + $tool = new Tool($webSearch); + $json = $this->assertJsonSerializeReturnsArray($tool); + + $this->assertJsonHasKeys($json, ['type', 'webSearch']); + $this->assertEquals(ToolTypeEnum::webSearch()->value, $json['type']); + $this->assertIsArray($json['webSearch']); + $this->assertArrayHasKey('allowedDomains', $json['webSearch']); + $this->assertArrayHasKey('blockedDomains', $json['webSearch']); + } + + /** + * Tests fromJson method with function declarations. + * + * @return void + */ + public function testFromJsonWithFunctionDeclarations(): void + { + $json = [ + 'type' => ToolTypeEnum::functionDeclarations()->value, + 'functionDeclarations' => [ + [ + 'name' => 'testFunc', + 'description' => 'Test function', + 'parameters' => ['type' => 'object'] + ] + ] + ]; + + $tool = Tool::fromJson($json); + + $this->assertInstanceOf(Tool::class, $tool); + $this->assertEquals(ToolTypeEnum::functionDeclarations(), $tool->getType()); + $this->assertCount(1, $tool->getFunctionDeclarations()); + $this->assertEquals('testFunc', $tool->getFunctionDeclarations()[0]->getName()); + $this->assertNull($tool->getWebSearch()); + } + + /** + * Tests fromJson method with web search. + * + * @return void + */ + public function testFromJsonWithWebSearch(): void + { + $json = [ + 'type' => ToolTypeEnum::webSearch()->value, + 'webSearch' => [ + 'allowedDomains' => ['example.com'], + 'blockedDomains' => ['spam.com'] + ] + ]; + + $tool = Tool::fromJson($json); + + $this->assertInstanceOf(Tool::class, $tool); + $this->assertEquals(ToolTypeEnum::webSearch(), $tool->getType()); + $this->assertNotNull($tool->getWebSearch()); + $this->assertEquals(['example.com'], $tool->getWebSearch()->getAllowedDomains()); + $this->assertEquals(['spam.com'], $tool->getWebSearch()->getBlockedDomains()); + $this->assertNull($tool->getFunctionDeclarations()); + } + + /** + * Tests round-trip JSON serialization with function declarations. + * + * @return void + */ + public function testJsonRoundTripWithFunctionDeclarations(): void + { + $this->assertJsonRoundTrip( + new Tool([ + new FunctionDeclaration('calculate', 'Performs calculations', ['expr' => ['type' => 'string']]), + new FunctionDeclaration('validate', 'Validates input', ['data' => ['type' => 'object']]) + ]), + function ($original, $restored) { + $this->assertEquals($original->getType()->value, $restored->getType()->value); + $this->assertCount( + count($original->getFunctionDeclarations()), + $restored->getFunctionDeclarations() + ); + foreach ($original->getFunctionDeclarations() as $i => $origFunc) { + $restoredFunc = $restored->getFunctionDeclarations()[$i]; + $this->assertEquals($origFunc->getName(), $restoredFunc->getName()); + $this->assertEquals($origFunc->getDescription(), $restoredFunc->getDescription()); + $this->assertEquals($origFunc->getParameters(), $restoredFunc->getParameters()); + } + } + ); + } + + /** + * Tests round-trip JSON serialization with web search. + * + * @return void + */ + public function testJsonRoundTripWithWebSearch(): void + { + $this->assertJsonRoundTrip( + new Tool(new WebSearch(['docs.example.com'], ['ads.example.com'])), + function ($original, $restored) { + $this->assertEquals($original->getType()->value, $restored->getType()->value); + $this->assertEquals( + $original->getWebSearch()->getAllowedDomains(), + $restored->getWebSearch()->getAllowedDomains() + ); + $this->assertEquals( + $original->getWebSearch()->getBlockedDomains(), + $restored->getWebSearch()->getBlockedDomains() + ); + } + ); + } + + /** + * Tests Tool implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $tool = new Tool([]); + $this->assertImplementsJsonSerialization($tool); + } } \ No newline at end of file diff --git a/tests/unit/Tools/DTO/WebSearchTest.php b/tests/unit/Tools/DTO/WebSearchTest.php index 0a9aee97..141f0da0 100644 --- a/tests/unit/Tools/DTO/WebSearchTest.php +++ b/tests/unit/Tools/DTO/WebSearchTest.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Tests\unit\Tools\DTO; use PHPUnit\Framework\TestCase; +use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; use WordPress\AiClient\Tools\DTO\WebSearch; /** @@ -12,6 +13,7 @@ */ class WebSearchTest extends TestCase { + use JsonSerializationTestTrait; /** * Tests creating WebSearch with both allowed and disallowed domains. * @@ -291,4 +293,158 @@ public function testWithCommonDomainPatterns(): void $this->assertContains('stackoverflow.com', $webSearch->getAllowedDomains()); $this->assertContains('youtube.com', $webSearch->getDisallowedDomains()); } + + /** + * Tests JSON serialization with both domain lists. + * + * @return void + */ + public function testJsonSerializeWithBothDomainLists(): void + { + $webSearch = new WebSearch( + ['example.com', 'docs.example.com'], + ['spam.com', 'malware.com'] + ); + + $json = $this->assertJsonSerializeReturnsArray($webSearch); + + $this->assertJsonHasKeys($json, ['allowedDomains', 'disallowedDomains']); + $this->assertEquals(['example.com', 'docs.example.com'], $json['allowedDomains']); + $this->assertEquals(['spam.com', 'malware.com'], $json['disallowedDomains']); + } + + /** + * Tests JSON serialization with empty domain lists. + * + * @return void + */ + public function testJsonSerializeWithEmptyDomainLists(): void + { + $webSearch = new WebSearch(); + + $json = $this->assertJsonSerializeReturnsArray($webSearch); + + $this->assertJsonHasKeys($json, ['allowedDomains', 'disallowedDomains']); + $this->assertEquals([], $json['allowedDomains']); + $this->assertEquals([], $json['disallowedDomains']); + } + + /** + * Tests JSON serialization with only allowed domains. + * + * @return void + */ + public function testJsonSerializeWithOnlyAllowedDomains(): void + { + $webSearch = new WebSearch(['trusted1.com', 'trusted2.com']); + + $json = $this->assertJsonSerializeReturnsArray($webSearch); + + $this->assertJsonHasKeys($json, ['allowedDomains', 'disallowedDomains']); + $this->assertEquals(['trusted1.com', 'trusted2.com'], $json['allowedDomains']); + $this->assertEquals([], $json['disallowedDomains']); + } + + /** + * Tests fromJson method with both domain lists. + * + * @return void + */ + public function testFromJsonWithBothDomainLists(): void + { + $json = [ + 'allowedDomains' => ['api.example.com', 'docs.example.com'], + 'disallowedDomains' => ['ads.example.com', 'tracking.example.com'] + ]; + + $webSearch = WebSearch::fromJson($json); + + $this->assertInstanceOf(WebSearch::class, $webSearch); + $this->assertEquals(['api.example.com', 'docs.example.com'], $webSearch->getAllowedDomains()); + $this->assertEquals(['ads.example.com', 'tracking.example.com'], $webSearch->getDisallowedDomains()); + } + + /** + * Tests fromJson method with empty arrays. + * + * @return void + */ + public function testFromJsonWithEmptyArrays(): void + { + $json = [ + 'allowedDomains' => [], + 'disallowedDomains' => [] + ]; + + $webSearch = WebSearch::fromJson($json); + + $this->assertInstanceOf(WebSearch::class, $webSearch); + $this->assertEquals([], $webSearch->getAllowedDomains()); + $this->assertEquals([], $webSearch->getDisallowedDomains()); + } + + /** + * Tests fromJson method with missing fields uses defaults. + * + * @return void + */ + public function testFromJsonWithMissingFieldsUsesDefaults(): void + { + $json = []; + + $webSearch = WebSearch::fromJson($json); + + $this->assertInstanceOf(WebSearch::class, $webSearch); + $this->assertEquals([], $webSearch->getAllowedDomains()); + $this->assertEquals([], $webSearch->getDisallowedDomains()); + } + + /** + * Tests round-trip JSON serialization. + * + * @return void + */ + public function testJsonRoundTrip(): void + { + $this->assertJsonRoundTrip( + new WebSearch( + ['wikipedia.org', 'arxiv.org', 'pubmed.gov'], + ['facebook.com', 'twitter.com', 'instagram.com'] + ), + function ($original, $restored) { + $this->assertEquals($original->getAllowedDomains(), $restored->getAllowedDomains()); + $this->assertEquals($original->getDisallowedDomains(), $restored->getDisallowedDomains()); + } + ); + } + + /** + * Tests round-trip with special characters in domains. + * + * @return void + */ + public function testJsonRoundTripWithSpecialCharacters(): void + { + $this->assertJsonRoundTrip( + new WebSearch( + ['example-with-dash.com', 'sub.domain.example.com', '192.168.1.1'], + ['bad_underscore.com', 'another-dash.org'] + ), + function ($original, $restored) { + $this->assertEquals($original->getAllowedDomains(), $restored->getAllowedDomains()); + $this->assertEquals($original->getDisallowedDomains(), $restored->getDisallowedDomains()); + } + ); + } + + /** + * Tests WebSearch implements WithJsonSerialization. + * + * @return void + */ + public function testImplementsWithJsonSerialization(): void + { + $webSearch = new WebSearch(); + $this->assertImplementsJsonSerialization($webSearch); + } } \ No newline at end of file From a350b575f5bf9e555f8671f3328be31a0c630518 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 10:11:05 -0600 Subject: [PATCH 05/36] feat: improves fromJson validation and documenting --- src/Files/DTO/File.php | 12 +++++++- src/Messages/DTO/Message.php | 4 ++- src/Messages/DTO/MessagePart.php | 28 ++++++++++++++++--- src/Messages/DTO/ModelMessage.php | 2 +- src/Messages/DTO/SystemMessage.php | 2 +- src/Messages/DTO/UserMessage.php | 2 +- src/Operations/DTO/GenerativeAiOperation.php | 4 ++- src/Results/DTO/Candidate.php | 4 ++- src/Results/DTO/GenerativeAiResult.php | 11 ++++++-- src/Results/DTO/TokenUsage.php | 6 ++++ src/Tools/DTO/FunctionCall.php | 2 ++ src/Tools/DTO/FunctionDeclaration.php | 2 ++ src/Tools/DTO/FunctionResponse.php | 2 ++ src/Tools/DTO/Tool.php | 16 +++++++++-- .../DTO/GenerativeAiOperationTest.php | 4 +-- .../Results/DTO/GenerativeAiResultTest.php | 8 +++--- tests/unit/Tools/DTO/ToolTest.php | 10 +++---- 17 files changed, 93 insertions(+), 26 deletions(-) diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index 3fe47a45..992438ec 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -406,16 +406,26 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{fileType: string, url?: string, mimeType?: string, base64Data?: string} $json The JSON data. */ public static function fromJson(array $json): File { $fileType = FileTypeEnum::from((string) $json['fileType']); if ($fileType->isRemote()) { + if (!isset($json['url']) || !isset($json['mimeType'])) { + throw new \InvalidArgumentException('Remote file requires url and mimeType.'); + } return new self((string) $json['url'], (string) $json['mimeType']); } else { + if (!isset($json['mimeType']) || !isset($json['base64Data'])) { + throw new \InvalidArgumentException('Inline file requires mimeType and base64Data.'); + } // Create data URI from base64 data and mime type - $dataUri = sprintf('data:%s;base64,%s', (string) $json['mimeType'], (string) $json['base64Data']); + $mimeType = (string) $json['mimeType']; + $base64Data = (string) $json['base64Data']; + $dataUri = sprintf('data:%s;base64,%s', $mimeType, $base64Data); return new self($dataUri); } } diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index 6dce8a1c..01bb6203 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -113,11 +113,13 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{role: string, parts: array>} $json The JSON data. */ public static function fromJson(array $json): Message { $role = MessageRoleEnum::from((string) $json['role']); - /** @var array> $partsData */ + /** @var array, functionCall?: array, functionResponse?: array}> $partsData */ $partsData = $json['parts']; $parts = array_map(function (array $partData) { return MessagePart::fromJson($partData); diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index 9971b1b8..d105a2ec 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -232,27 +232,47 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{ + * type: string, + * text?: string, + * file?: array, + * functionCall?: array, + * functionResponse?: array + * } $json The JSON data. */ public static function fromJson(array $json): MessagePart { $type = MessagePartTypeEnum::from((string) $json['type']); if ($type->isText()) { + if (!isset($json['text'])) { + throw new \InvalidArgumentException('Text message part requires text field.'); + } return new self((string) $json['text']); } elseif ($type->isFile()) { - /** @var array $fileData */ + if (!isset($json['file'])) { + throw new \InvalidArgumentException('File message part requires file field.'); + } + /** @var array{fileType: string, url?: string, mimeType?: string, base64Data?: string} $fileData */ $fileData = $json['file']; return new self(File::fromJson($fileData)); } elseif ($type->isFunctionCall()) { - /** @var array $functionCallData */ + if (!isset($json['functionCall'])) { + throw new \InvalidArgumentException('Function call message part requires functionCall field.'); + } + /** @var array{id?: string, name?: string, args?: mixed} $functionCallData */ $functionCallData = $json['functionCall']; return new self(FunctionCall::fromJson($functionCallData)); } elseif ($type->isFunctionResponse()) { - /** @var array $functionResponseData */ + if (!isset($json['functionResponse'])) { + throw new \InvalidArgumentException('Function response message part requires functionResponse field.'); + } + /** @var array{id: string, name: string, response: mixed} $functionResponseData */ $functionResponseData = $json['functionResponse']; return new self(FunctionResponse::fromJson($functionResponseData)); } - throw new \InvalidArgumentException(sprintf('Unknown message part type: %s', $json['type'])); + throw new \InvalidArgumentException(sprintf('Unknown message part type: %s', (string) $json['type'])); } } diff --git a/src/Messages/DTO/ModelMessage.php b/src/Messages/DTO/ModelMessage.php index 239029f7..94382769 100644 --- a/src/Messages/DTO/ModelMessage.php +++ b/src/Messages/DTO/ModelMessage.php @@ -35,7 +35,7 @@ public function __construct(array $parts) */ public static function fromJson(array $json): ModelMessage { - /** @var array> $partsData */ + /** @var array, functionCall?: array, functionResponse?: array}> $partsData */ $partsData = $json['parts']; $parts = array_map(function (array $partData) { return MessagePart::fromJson($partData); diff --git a/src/Messages/DTO/SystemMessage.php b/src/Messages/DTO/SystemMessage.php index 3f1c3f5b..b6fcaa40 100644 --- a/src/Messages/DTO/SystemMessage.php +++ b/src/Messages/DTO/SystemMessage.php @@ -35,7 +35,7 @@ public function __construct(array $parts) */ public static function fromJson(array $json): SystemMessage { - /** @var array> $partsData */ + /** @var array, functionCall?: array, functionResponse?: array}> $partsData */ $partsData = $json['parts']; $parts = array_map(function (array $partData) { return MessagePart::fromJson($partData); diff --git a/src/Messages/DTO/UserMessage.php b/src/Messages/DTO/UserMessage.php index 859303cd..e5b57214 100644 --- a/src/Messages/DTO/UserMessage.php +++ b/src/Messages/DTO/UserMessage.php @@ -34,7 +34,7 @@ public function __construct(array $parts) */ public static function fromJson(array $json): UserMessage { - /** @var array> $partsData */ + /** @var array, functionCall?: array, functionResponse?: array}> $partsData */ $partsData = $json['parts']; $parts = array_map(function (array $partData) { return MessagePart::fromJson($partData); diff --git a/src/Operations/DTO/GenerativeAiOperation.php b/src/Operations/DTO/GenerativeAiOperation.php index 3cd88c47..ef8e859a 100644 --- a/src/Operations/DTO/GenerativeAiOperation.php +++ b/src/Operations/DTO/GenerativeAiOperation.php @@ -158,13 +158,15 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{id: string, state: string, result?: array} $json The JSON data. */ public static function fromJson(array $json): GenerativeAiOperation { $state = OperationStateEnum::from((string) $json['state']); $result = null; if (isset($json['result'])) { - /** @var array $resultData */ + /** @var array{id: string, candidates: array>, tokenUsage: array, providerMetadata?: array} $resultData */ $resultData = $json['result']; $result = GenerativeAiResult::fromJson($resultData); } diff --git a/src/Results/DTO/Candidate.php b/src/Results/DTO/Candidate.php index b713fde4..6864930b 100644 --- a/src/Results/DTO/Candidate.php +++ b/src/Results/DTO/Candidate.php @@ -137,10 +137,12 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{message: array, finishReason: string, tokenCount: int|string} $json The JSON data. */ public static function fromJson(array $json): Candidate { - /** @var array $messageData */ + /** @var array{role: string, parts: array>} $messageData */ $messageData = $json['message']; return new self( diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index acae7be1..c0c0c543 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -402,16 +402,23 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{ + * id: string, + * candidates: array>, + * tokenUsage: array, + * providerMetadata?: array + * } $json The JSON data. */ public static function fromJson(array $json): GenerativeAiResult { - /** @var array> $candidatesData */ + /** @var array, finishReason: string, tokenCount: int|string}> $candidatesData */ $candidatesData = $json['candidates']; $candidates = array_map(function (array $candidateData) { return Candidate::fromJson($candidateData); }, $candidatesData); - /** @var array $tokenUsageData */ + /** @var array{promptTokens: int|string, completionTokens: int|string, totalTokens: int|string} $tokenUsageData */ $tokenUsageData = $json['tokenUsage']; /** @var array $providerMetadata */ $providerMetadata = $json['providerMetadata'] ?? []; diff --git a/src/Results/DTO/TokenUsage.php b/src/Results/DTO/TokenUsage.php index 0c7f5a04..3d1b4da0 100644 --- a/src/Results/DTO/TokenUsage.php +++ b/src/Results/DTO/TokenUsage.php @@ -131,6 +131,12 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{ + * promptTokens: int|string, + * completionTokens: int|string, + * totalTokens: int|string + * } $json The JSON data. */ public static function fromJson(array $json): TokenUsage { diff --git a/src/Tools/DTO/FunctionCall.php b/src/Tools/DTO/FunctionCall.php index 391ff368..9d8d19e1 100644 --- a/src/Tools/DTO/FunctionCall.php +++ b/src/Tools/DTO/FunctionCall.php @@ -157,6 +157,8 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{id?: string, name?: string, args?: mixed} $json The JSON data. */ public static function fromJson(array $json): FunctionCall { diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index 19e078dd..d70378ad 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -136,6 +136,8 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{name: string, description: string, parameters?: mixed} $json The JSON data. */ public static function fromJson(array $json): FunctionDeclaration { diff --git a/src/Tools/DTO/FunctionResponse.php b/src/Tools/DTO/FunctionResponse.php index 94f30a0d..97368931 100644 --- a/src/Tools/DTO/FunctionResponse.php +++ b/src/Tools/DTO/FunctionResponse.php @@ -131,6 +131,8 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{id: string, name: string, response: mixed} $json The JSON data. */ public static function fromJson(array $json): FunctionResponse { diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index f6041dbe..36f87900 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -160,24 +160,36 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @param array{ + * type: string, + * functionDeclarations?: array>, + * webSearch?: array + * } $json The JSON data. */ public static function fromJson(array $json): Tool { $type = ToolTypeEnum::from((string) $json['type']); if ($type->isFunctionDeclarations()) { - /** @var array> $declarationsData */ + if (!isset($json['functionDeclarations'])) { + throw new \InvalidArgumentException('Function declarations tool requires functionDeclarations field.'); + } + /** @var array $declarationsData */ $declarationsData = $json['functionDeclarations']; $declarations = array_map(function (array $declarationData) { return FunctionDeclaration::fromJson($declarationData); }, $declarationsData); return new self($declarations); } elseif ($type->isWebSearch()) { + if (!isset($json['webSearch'])) { + throw new \InvalidArgumentException('Web search tool requires webSearch field.'); + } /** @var array $webSearchData */ $webSearchData = $json['webSearch']; return new self(WebSearch::fromJson($webSearchData)); } - throw new \InvalidArgumentException(sprintf('Unknown tool type: %s', $json['type'])); + throw new \InvalidArgumentException(sprintf('Unknown tool type: %s', (string) $json['type'])); } } diff --git a/tests/unit/Operations/DTO/GenerativeAiOperationTest.php b/tests/unit/Operations/DTO/GenerativeAiOperationTest.php index 2fdd9680..9287baad 100644 --- a/tests/unit/Operations/DTO/GenerativeAiOperationTest.php +++ b/tests/unit/Operations/DTO/GenerativeAiOperationTest.php @@ -391,8 +391,8 @@ public function testFromJsonSucceededState(): void ] ], 'tokenUsage' => [ - 'inputTokens' => 10, - 'outputTokens' => 30, + 'promptTokens' => 10, + 'completionTokens' => 30, 'totalTokens' => 40 ] ] diff --git a/tests/unit/Results/DTO/GenerativeAiResultTest.php b/tests/unit/Results/DTO/GenerativeAiResultTest.php index c6ce4589..efd4d79f 100644 --- a/tests/unit/Results/DTO/GenerativeAiResultTest.php +++ b/tests/unit/Results/DTO/GenerativeAiResultTest.php @@ -655,8 +655,8 @@ public function testFromJson(): void ] ], 'tokenUsage' => [ - 'inputTokens' => 8, - 'outputTokens' => 20, + 'promptTokens' => 8, + 'completionTokens' => 20, 'totalTokens' => 28 ], 'providerMetadata' => ['provider' => 'test'] @@ -667,8 +667,8 @@ public function testFromJson(): void $this->assertInstanceOf(GenerativeAiResult::class, $result); $this->assertEquals('result_from_json', $result->getId()); $this->assertCount(1, $result->getCandidates()); - $this->assertEquals(8, $result->getTokenUsage()->getInputTokens()); - $this->assertEquals(20, $result->getTokenUsage()->getOutputTokens()); + $this->assertEquals(8, $result->getTokenUsage()->getPromptTokens()); + $this->assertEquals(20, $result->getTokenUsage()->getCompletionTokens()); $this->assertEquals(28, $result->getTokenUsage()->getTotalTokens()); $this->assertEquals(['provider' => 'test'], $result->getProviderMetadata()); } diff --git a/tests/unit/Tools/DTO/ToolTest.php b/tests/unit/Tools/DTO/ToolTest.php index 911278db..6c74c905 100644 --- a/tests/unit/Tools/DTO/ToolTest.php +++ b/tests/unit/Tools/DTO/ToolTest.php @@ -340,7 +340,7 @@ public function testJsonSerializeWithWebSearch(): void $this->assertEquals(ToolTypeEnum::webSearch()->value, $json['type']); $this->assertIsArray($json['webSearch']); $this->assertArrayHasKey('allowedDomains', $json['webSearch']); - $this->assertArrayHasKey('blockedDomains', $json['webSearch']); + $this->assertArrayHasKey('disallowedDomains', $json['webSearch']); } /** @@ -381,7 +381,7 @@ public function testFromJsonWithWebSearch(): void 'type' => ToolTypeEnum::webSearch()->value, 'webSearch' => [ 'allowedDomains' => ['example.com'], - 'blockedDomains' => ['spam.com'] + 'disallowedDomains' => ['spam.com'] ] ]; @@ -391,7 +391,7 @@ public function testFromJsonWithWebSearch(): void $this->assertEquals(ToolTypeEnum::webSearch(), $tool->getType()); $this->assertNotNull($tool->getWebSearch()); $this->assertEquals(['example.com'], $tool->getWebSearch()->getAllowedDomains()); - $this->assertEquals(['spam.com'], $tool->getWebSearch()->getBlockedDomains()); + $this->assertEquals(['spam.com'], $tool->getWebSearch()->getDisallowedDomains()); $this->assertNull($tool->getFunctionDeclarations()); } @@ -439,8 +439,8 @@ function ($original, $restored) { $restored->getWebSearch()->getAllowedDomains() ); $this->assertEquals( - $original->getWebSearch()->getBlockedDomains(), - $restored->getWebSearch()->getBlockedDomains() + $original->getWebSearch()->getDisallowedDomains(), + $restored->getWebSearch()->getDisallowedDomains() ); } ); From f6a1605d919167a751201adc94969739998a5abd Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 10:41:17 -0600 Subject: [PATCH 06/36] refactor: clean up typing with generics --- .../Contracts/WithJsonSerialization.php | 6 ++-- src/Files/DTO/File.php | 19 ++++++++---- src/Messages/DTO/Message.php | 14 ++++++--- src/Messages/DTO/MessagePart.php | 31 ++++++++++--------- src/Messages/DTO/ModelMessage.php | 1 - src/Messages/DTO/SystemMessage.php | 1 - src/Messages/DTO/UserMessage.php | 1 - .../Contracts/OperationInterface.php | 2 ++ src/Operations/DTO/GenerativeAiOperation.php | 13 +++++--- src/Results/Contracts/ResultInterface.php | 2 ++ src/Results/DTO/Candidate.php | 13 +++++--- src/Results/DTO/GenerativeAiResult.php | 28 +++++++++++------ src/Results/DTO/TokenUsage.php | 14 +++++---- src/Tools/DTO/FunctionCall.php | 10 +++--- src/Tools/DTO/FunctionDeclaration.php | 10 +++--- src/Tools/DTO/FunctionResponse.php | 10 +++--- src/Tools/DTO/Tool.php | 23 ++++++++------ src/Tools/DTO/WebSearch.php | 6 ++-- 18 files changed, 125 insertions(+), 79 deletions(-) diff --git a/src/Common/Contracts/WithJsonSerialization.php b/src/Common/Contracts/WithJsonSerialization.php index 73f98da2..4e5b857b 100644 --- a/src/Common/Contracts/WithJsonSerialization.php +++ b/src/Common/Contracts/WithJsonSerialization.php @@ -10,6 +10,8 @@ * Interface for objects that support JSON serialization and deserialization. * * @since 1.0.0 + * + * @template TJsonShape of array */ interface WithJsonSerialization extends JsonSerializable { @@ -18,8 +20,8 @@ interface WithJsonSerialization extends JsonSerializable * * @since 1.0.0 * - * @param array $json The JSON data. - * @return self The created instance. + * @param TJsonShape $json The JSON data. + * @return static The created instance. */ public static function fromJson(array $json); } diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index 992438ec..9ab6d3bd 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -16,6 +16,15 @@ * and handles them appropriately. * * @since n.e.x.t + * + * @phpstan-type FileJsonShape array{ + * fileType: string, + * url?: string, + * mimeType?: string, + * base64Data?: string + * } + * + * @implements WithJsonSerialization */ class File implements WithJsonSchemaInterface, WithJsonSerialization { @@ -406,25 +415,23 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{fileType: string, url?: string, mimeType?: string, base64Data?: string} $json The JSON data. */ public static function fromJson(array $json): File { - $fileType = FileTypeEnum::from((string) $json['fileType']); + $fileType = FileTypeEnum::from($json['fileType']); if ($fileType->isRemote()) { if (!isset($json['url']) || !isset($json['mimeType'])) { throw new \InvalidArgumentException('Remote file requires url and mimeType.'); } - return new self((string) $json['url'], (string) $json['mimeType']); + return new self($json['url'], $json['mimeType']); } else { if (!isset($json['mimeType']) || !isset($json['base64Data'])) { throw new \InvalidArgumentException('Inline file requires mimeType and base64Data.'); } // Create data URI from base64 data and mime type - $mimeType = (string) $json['mimeType']; - $base64Data = (string) $json['base64Data']; + $mimeType = $json['mimeType']; + $base64Data = $json['base64Data']; $dataUri = sprintf('data:%s;base64,%s', $mimeType, $base64Data); return new self($dataUri); } diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index 01bb6203..e51b621d 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -15,6 +15,15 @@ * containing a role and one or more parts with different content types. * * @since n.e.x.t + * + * @phpstan-import-type MessagePartJsonShape from MessagePart + * + * @phpstan-type MessageJsonShape array{ + * role: string, + * parts: array + * } + * + * @implements WithJsonSerialization */ class Message implements WithJsonSchemaInterface, WithJsonSerialization { @@ -113,13 +122,10 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{role: string, parts: array>} $json The JSON data. */ public static function fromJson(array $json): Message { - $role = MessageRoleEnum::from((string) $json['role']); - /** @var array, functionCall?: array, functionResponse?: array}> $partsData */ + $role = MessageRoleEnum::from($json['role']); $partsData = $json['parts']; $parts = array_map(function (array $partData) { return MessagePart::fromJson($partData); diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index d105a2ec..62d4e8b7 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -18,6 +18,20 @@ * function calls, etc. This DTO encapsulates one such part. * * @since n.e.x.t + * + * @phpstan-import-type FileJsonShape from File + * @phpstan-import-type FunctionCallJsonShape from FunctionCall + * @phpstan-import-type FunctionResponseJsonShape from FunctionResponse + * + * @phpstan-type MessagePartJsonShape array{ + * type: string, + * text?: string, + * file?: FileJsonShape, + * functionCall?: FunctionCallJsonShape, + * functionResponse?: FunctionResponseJsonShape + * } + * + * @implements WithJsonSerialization */ class MessagePart implements WithJsonSchemaInterface, WithJsonSerialization { @@ -232,47 +246,36 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{ - * type: string, - * text?: string, - * file?: array, - * functionCall?: array, - * functionResponse?: array - * } $json The JSON data. */ public static function fromJson(array $json): MessagePart { - $type = MessagePartTypeEnum::from((string) $json['type']); + $type = MessagePartTypeEnum::from($json['type']); if ($type->isText()) { if (!isset($json['text'])) { throw new \InvalidArgumentException('Text message part requires text field.'); } - return new self((string) $json['text']); + return new self($json['text']); } elseif ($type->isFile()) { if (!isset($json['file'])) { throw new \InvalidArgumentException('File message part requires file field.'); } - /** @var array{fileType: string, url?: string, mimeType?: string, base64Data?: string} $fileData */ $fileData = $json['file']; return new self(File::fromJson($fileData)); } elseif ($type->isFunctionCall()) { if (!isset($json['functionCall'])) { throw new \InvalidArgumentException('Function call message part requires functionCall field.'); } - /** @var array{id?: string, name?: string, args?: mixed} $functionCallData */ $functionCallData = $json['functionCall']; return new self(FunctionCall::fromJson($functionCallData)); } elseif ($type->isFunctionResponse()) { if (!isset($json['functionResponse'])) { throw new \InvalidArgumentException('Function response message part requires functionResponse field.'); } - /** @var array{id: string, name: string, response: mixed} $functionResponseData */ $functionResponseData = $json['functionResponse']; return new self(FunctionResponse::fromJson($functionResponseData)); } - throw new \InvalidArgumentException(sprintf('Unknown message part type: %s', (string) $json['type'])); + throw new \InvalidArgumentException(sprintf('Unknown message part type: %s', $json['type'])); } } diff --git a/src/Messages/DTO/ModelMessage.php b/src/Messages/DTO/ModelMessage.php index 94382769..7905c70a 100644 --- a/src/Messages/DTO/ModelMessage.php +++ b/src/Messages/DTO/ModelMessage.php @@ -35,7 +35,6 @@ public function __construct(array $parts) */ public static function fromJson(array $json): ModelMessage { - /** @var array, functionCall?: array, functionResponse?: array}> $partsData */ $partsData = $json['parts']; $parts = array_map(function (array $partData) { return MessagePart::fromJson($partData); diff --git a/src/Messages/DTO/SystemMessage.php b/src/Messages/DTO/SystemMessage.php index b6fcaa40..8c6ad682 100644 --- a/src/Messages/DTO/SystemMessage.php +++ b/src/Messages/DTO/SystemMessage.php @@ -35,7 +35,6 @@ public function __construct(array $parts) */ public static function fromJson(array $json): SystemMessage { - /** @var array, functionCall?: array, functionResponse?: array}> $partsData */ $partsData = $json['parts']; $parts = array_map(function (array $partData) { return MessagePart::fromJson($partData); diff --git a/src/Messages/DTO/UserMessage.php b/src/Messages/DTO/UserMessage.php index e5b57214..88c1c2ec 100644 --- a/src/Messages/DTO/UserMessage.php +++ b/src/Messages/DTO/UserMessage.php @@ -34,7 +34,6 @@ public function __construct(array $parts) */ public static function fromJson(array $json): UserMessage { - /** @var array, functionCall?: array, functionResponse?: array}> $partsData */ $partsData = $json['parts']; $parts = array_map(function (array $partData) { return MessagePart::fromJson($partData); diff --git a/src/Operations/Contracts/OperationInterface.php b/src/Operations/Contracts/OperationInterface.php index 4fa9ab7a..b5369b36 100644 --- a/src/Operations/Contracts/OperationInterface.php +++ b/src/Operations/Contracts/OperationInterface.php @@ -15,6 +15,8 @@ * They provide a way to track the progress and retrieve results asynchronously. * * @since n.e.x.t + * + * @extends WithJsonSerialization> */ interface OperationInterface extends WithJsonSchemaInterface, WithJsonSerialization { diff --git a/src/Operations/DTO/GenerativeAiOperation.php b/src/Operations/DTO/GenerativeAiOperation.php index ef8e859a..adda3b5d 100644 --- a/src/Operations/DTO/GenerativeAiOperation.php +++ b/src/Operations/DTO/GenerativeAiOperation.php @@ -15,6 +15,12 @@ * immediately, providing access to the result once available. * * @since n.e.x.t + * + * @phpstan-import-type GenerativeAiResultJsonShape from GenerativeAiResult + * + * @phpstan-type GenerativeAiOperationJsonShape array{id: string, state: string, result?: GenerativeAiResultJsonShape} + * + * @implements WithJsonSerialization */ class GenerativeAiOperation implements OperationInterface { @@ -158,19 +164,16 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{id: string, state: string, result?: array} $json The JSON data. */ public static function fromJson(array $json): GenerativeAiOperation { - $state = OperationStateEnum::from((string) $json['state']); + $state = OperationStateEnum::from($json['state']); $result = null; if (isset($json['result'])) { - /** @var array{id: string, candidates: array>, tokenUsage: array, providerMetadata?: array} $resultData */ $resultData = $json['result']; $result = GenerativeAiResult::fromJson($resultData); } - return new self((string) $json['id'], $state, $result); + return new self($json['id'], $state, $result); } } diff --git a/src/Results/Contracts/ResultInterface.php b/src/Results/Contracts/ResultInterface.php index 49140e07..821dfab2 100644 --- a/src/Results/Contracts/ResultInterface.php +++ b/src/Results/Contracts/ResultInterface.php @@ -15,6 +15,8 @@ * such as token usage and provider-specific information. * * @since n.e.x.t + * + * @extends WithJsonSerialization> */ interface ResultInterface extends WithJsonSchemaInterface, WithJsonSerialization { diff --git a/src/Results/DTO/Candidate.php b/src/Results/DTO/Candidate.php index 6864930b..0d05606b 100644 --- a/src/Results/DTO/Candidate.php +++ b/src/Results/DTO/Candidate.php @@ -16,6 +16,12 @@ * Each candidate contains a message and metadata about why generation stopped. * * @since n.e.x.t + * + * @phpstan-import-type MessageJsonShape from Message + * + * @phpstan-type CandidateJsonShape array{message: MessageJsonShape, finishReason: string, tokenCount: int|string} + * + * @implements WithJsonSerialization */ class Candidate implements WithJsonSchemaInterface, WithJsonSerialization { @@ -137,18 +143,15 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{message: array, finishReason: string, tokenCount: int|string} $json The JSON data. */ public static function fromJson(array $json): Candidate { - /** @var array{role: string, parts: array>} $messageData */ $messageData = $json['message']; return new self( Message::fromJson($messageData), - FinishReasonEnum::from((string) $json['finishReason']), - (int) $json['tokenCount'] + FinishReasonEnum::from($json['finishReason']), + $json['tokenCount'] ); } } diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index c0c0c543..6caaf0b0 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -16,6 +16,18 @@ * and metadata from the AI provider. * * @since n.e.x.t + * + * @phpstan-import-type CandidateJsonShape from Candidate + * @phpstan-import-type TokenUsageJsonShape from TokenUsage + * + * @phpstan-type GenerativeAiResultJsonShape array{ + * id: string, + * candidates: array, + * tokenUsage: TokenUsageJsonShape, + * providerMetadata?: array + * } + * + * @implements WithJsonSerialization */ class GenerativeAiResult implements ResultInterface { @@ -402,29 +414,25 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{ - * id: string, - * candidates: array>, - * tokenUsage: array, - * providerMetadata?: array - * } $json The JSON data. */ public static function fromJson(array $json): GenerativeAiResult { - /** @var array, finishReason: string, tokenCount: int|string}> $candidatesData */ + /** @var array $candidatesData */ $candidatesData = $json['candidates']; $candidates = array_map(function (array $candidateData) { return Candidate::fromJson($candidateData); }, $candidatesData); - /** @var array{promptTokens: int|string, completionTokens: int|string, totalTokens: int|string} $tokenUsageData */ + /** @var TokenUsageJsonShape $tokenUsageData */ $tokenUsageData = $json['tokenUsage']; /** @var array $providerMetadata */ $providerMetadata = $json['providerMetadata'] ?? []; + /** @var string $id */ + $id = $json['id']; + return new self( - (string) $json['id'], + $id, $candidates, TokenUsage::fromJson($tokenUsageData), $providerMetadata diff --git a/src/Results/DTO/TokenUsage.php b/src/Results/DTO/TokenUsage.php index 3d1b4da0..95648f15 100644 --- a/src/Results/DTO/TokenUsage.php +++ b/src/Results/DTO/TokenUsage.php @@ -14,6 +14,14 @@ * which is important for monitoring usage and costs. * * @since n.e.x.t + * + * @phpstan-type TokenUsageJsonShape array{ + * promptTokens: int|string, + * completionTokens: int|string, + * totalTokens: int|string + * } + * + * @implements WithJsonSerialization */ class TokenUsage implements WithJsonSchemaInterface, WithJsonSerialization { @@ -131,12 +139,6 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{ - * promptTokens: int|string, - * completionTokens: int|string, - * totalTokens: int|string - * } $json The JSON data. */ public static function fromJson(array $json): TokenUsage { diff --git a/src/Tools/DTO/FunctionCall.php b/src/Tools/DTO/FunctionCall.php index 9d8d19e1..3b263248 100644 --- a/src/Tools/DTO/FunctionCall.php +++ b/src/Tools/DTO/FunctionCall.php @@ -14,6 +14,10 @@ * wants to invoke, including the function name and its arguments. * * @since n.e.x.t + * + * @phpstan-type FunctionCallJsonShape array{id?: string, name?: string, args?: mixed} + * + * @implements WithJsonSerialization */ class FunctionCall implements WithJsonSchemaInterface, WithJsonSerialization { @@ -157,8 +161,6 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{id?: string, name?: string, args?: mixed} $json The JSON data. */ public static function fromJson(array $json): FunctionCall { @@ -166,8 +168,8 @@ public static function fromJson(array $json): FunctionCall $args = $json['args'] ?? []; return new self( - isset($json['id']) ? (string) $json['id'] : null, - isset($json['name']) ? (string) $json['name'] : null, + isset($json['id']) ? $json['id'] : null, + isset($json['name']) ? $json['name'] : null, $args ); } diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index d70378ad..b96fec1d 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -14,6 +14,10 @@ * including its name, description, and parameter schema. * * @since n.e.x.t + * + * @phpstan-type FunctionDeclarationJsonShape array{name: string, description: string, parameters?: mixed} + * + * @implements WithJsonSerialization */ class FunctionDeclaration implements WithJsonSchemaInterface, WithJsonSerialization { @@ -136,14 +140,12 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{name: string, description: string, parameters?: mixed} $json The JSON data. */ public static function fromJson(array $json): FunctionDeclaration { return new self( - (string) $json['name'], - (string) $json['description'], + $json['name'], + $json['description'], $json['parameters'] ?? null ); } diff --git a/src/Tools/DTO/FunctionResponse.php b/src/Tools/DTO/FunctionResponse.php index 97368931..c4425d00 100644 --- a/src/Tools/DTO/FunctionResponse.php +++ b/src/Tools/DTO/FunctionResponse.php @@ -14,6 +14,10 @@ * requested by the AI model through a FunctionCall. * * @since n.e.x.t + * + * @phpstan-type FunctionResponseJsonShape array{id: string, name: string, response: mixed} + * + * @implements WithJsonSerialization */ class FunctionResponse implements WithJsonSchemaInterface, WithJsonSerialization { @@ -131,14 +135,12 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{id: string, name: string, response: mixed} $json The JSON data. */ public static function fromJson(array $json): FunctionResponse { return new self( - (string) $json['id'], - (string) $json['name'], + $json['id'], + $json['name'], $json['response'] ); } diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index 36f87900..9cc100d1 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -15,6 +15,17 @@ * such as calling functions or performing web searches. * * @since n.e.x.t + * + * @phpstan-import-type FunctionDeclarationJsonShape from FunctionDeclaration + * @phpstan-import-type WebSearchJsonShape from WebSearch + * + * @phpstan-type ToolJsonShape array{ + * type: string, + * functionDeclarations?: array, + * webSearch?: WebSearchJsonShape + * } + * + * @implements WithJsonSerialization */ class Tool implements WithJsonSchemaInterface, WithJsonSerialization { @@ -160,22 +171,15 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t - * - * @param array{ - * type: string, - * functionDeclarations?: array>, - * webSearch?: array - * } $json The JSON data. */ public static function fromJson(array $json): Tool { - $type = ToolTypeEnum::from((string) $json['type']); + $type = ToolTypeEnum::from($json['type']); if ($type->isFunctionDeclarations()) { if (!isset($json['functionDeclarations'])) { throw new \InvalidArgumentException('Function declarations tool requires functionDeclarations field.'); } - /** @var array $declarationsData */ $declarationsData = $json['functionDeclarations']; $declarations = array_map(function (array $declarationData) { return FunctionDeclaration::fromJson($declarationData); @@ -185,11 +189,10 @@ public static function fromJson(array $json): Tool if (!isset($json['webSearch'])) { throw new \InvalidArgumentException('Web search tool requires webSearch field.'); } - /** @var array $webSearchData */ $webSearchData = $json['webSearch']; return new self(WebSearch::fromJson($webSearchData)); } - throw new \InvalidArgumentException(sprintf('Unknown tool type: %s', (string) $json['type'])); + throw new \InvalidArgumentException(sprintf('Unknown tool type: %s', $json['type'])); } } diff --git a/src/Tools/DTO/WebSearch.php b/src/Tools/DTO/WebSearch.php index 02eb3e5f..d123feba 100644 --- a/src/Tools/DTO/WebSearch.php +++ b/src/Tools/DTO/WebSearch.php @@ -14,6 +14,10 @@ * including allowed and disallowed domains. * * @since n.e.x.t + * + * @phpstan-type WebSearchJsonShape array{allowedDomains?: string[], disallowedDomains?: string[]} + * + * @implements WithJsonSerialization */ class WebSearch implements WithJsonSchemaInterface, WithJsonSerialization { @@ -116,9 +120,7 @@ public function jsonSerialize(): array */ public static function fromJson(array $json): WebSearch { - /** @var string[] $allowedDomains */ $allowedDomains = $json['allowedDomains'] ?? []; - /** @var string[] $disallowedDomains */ $disallowedDomains = $json['disallowedDomains'] ?? []; return new self( From cf5b3040aeb8576da8b7dbb403123abeb97fa8d9 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 11:05:31 -0600 Subject: [PATCH 07/36] refactor: further cleaning up of types --- src/Files/DTO/File.php | 8 ++++---- src/Messages/DTO/Message.php | 17 +++++++++++++++-- src/Messages/DTO/MessagePart.php | 4 ++-- src/Messages/DTO/ModelMessage.php | 17 +---------------- src/Messages/DTO/SystemMessage.php | 17 +---------------- src/Messages/DTO/UserMessage.php | 17 +---------------- src/Operations/Contracts/OperationInterface.php | 4 +++- src/Operations/DTO/GenerativeAiOperation.php | 9 ++++----- src/Results/Contracts/ResultInterface.php | 4 +++- src/Results/DTO/Candidate.php | 6 +++--- src/Results/DTO/GenerativeAiResult.php | 14 ++++---------- src/Results/DTO/TokenUsage.php | 4 ++-- src/Tools/DTO/FunctionCall.php | 4 ++-- src/Tools/DTO/FunctionDeclaration.php | 4 ++-- src/Tools/DTO/FunctionResponse.php | 4 ++-- src/Tools/DTO/Tool.php | 4 ++-- src/Tools/DTO/WebSearch.php | 4 ++-- 17 files changed, 53 insertions(+), 88 deletions(-) diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index 9ab6d3bd..e97c9818 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -26,7 +26,7 @@ * * @implements WithJsonSerialization */ -class File implements WithJsonSchemaInterface, WithJsonSerialization +final class File implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var MimeType The MIME type of the file. @@ -393,7 +393,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return FileJsonShape */ public function jsonSerialize(): array { @@ -402,9 +402,9 @@ public function jsonSerialize(): array 'mimeType' => $this->getMimeType(), ]; - if ($this->fileType->isRemote()) { + if ($this->fileType->isRemote() && $this->url !== null) { $data['url'] = $this->url; - } else { + } elseif (!$this->fileType->isRemote() && $this->base64Data !== null) { $data['base64Data'] = $this->base64Data; } diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index e51b621d..144acb8a 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -106,7 +106,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return MessageJsonShape */ public function jsonSerialize(): array { @@ -123,7 +123,7 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): Message + final public static function fromJson(array $json): Message { $role = MessageRoleEnum::from($json['role']); $partsData = $json['parts']; @@ -131,6 +131,19 @@ public static function fromJson(array $json): Message return MessagePart::fromJson($partData); }, $partsData); + // Determine which concrete class to instantiate based on role + if ($role->isUser()) { + /** @phpstan-ignore-next-line */ + return new UserMessage($parts); + } elseif ($role->isModel()) { + /** @phpstan-ignore-next-line */ + return new ModelMessage($parts); + } elseif ($role->isSystem()) { + /** @phpstan-ignore-next-line */ + return new SystemMessage($parts); + } + + /** @phpstan-ignore-next-line */ return new self($role, $parts); } } diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index 62d4e8b7..d8d9c299 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -33,7 +33,7 @@ * * @implements WithJsonSerialization */ -class MessagePart implements WithJsonSchemaInterface, WithJsonSerialization +final class MessagePart implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var MessagePartTypeEnum The type of this message part. @@ -223,7 +223,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return MessagePartJsonShape */ public function jsonSerialize(): array { diff --git a/src/Messages/DTO/ModelMessage.php b/src/Messages/DTO/ModelMessage.php index 7905c70a..f683247d 100644 --- a/src/Messages/DTO/ModelMessage.php +++ b/src/Messages/DTO/ModelMessage.php @@ -14,7 +14,7 @@ * * @since n.e.x.t */ -class ModelMessage extends Message +final class ModelMessage extends Message { /** * Constructor. @@ -27,19 +27,4 @@ public function __construct(array $parts) { parent::__construct(MessageRoleEnum::model(), $parts); } - - /** - * {@inheritDoc} - * - * @since n.e.x.t - */ - public static function fromJson(array $json): ModelMessage - { - $partsData = $json['parts']; - $parts = array_map(function (array $partData) { - return MessagePart::fromJson($partData); - }, $partsData); - - return new self($parts); - } } diff --git a/src/Messages/DTO/SystemMessage.php b/src/Messages/DTO/SystemMessage.php index 8c6ad682..444d2834 100644 --- a/src/Messages/DTO/SystemMessage.php +++ b/src/Messages/DTO/SystemMessage.php @@ -14,7 +14,7 @@ * * @since n.e.x.t */ -class SystemMessage extends Message +final class SystemMessage extends Message { /** * Constructor. @@ -27,19 +27,4 @@ public function __construct(array $parts) { parent::__construct(MessageRoleEnum::system(), $parts); } - - /** - * {@inheritDoc} - * - * @since n.e.x.t - */ - public static function fromJson(array $json): SystemMessage - { - $partsData = $json['parts']; - $parts = array_map(function (array $partData) { - return MessagePart::fromJson($partData); - }, $partsData); - - return new self($parts); - } } diff --git a/src/Messages/DTO/UserMessage.php b/src/Messages/DTO/UserMessage.php index 88c1c2ec..28bc30bb 100644 --- a/src/Messages/DTO/UserMessage.php +++ b/src/Messages/DTO/UserMessage.php @@ -13,7 +13,7 @@ * * @since n.e.x.t */ -class UserMessage extends Message +final class UserMessage extends Message { /** * Constructor. @@ -26,19 +26,4 @@ public function __construct(array $parts) { parent::__construct(MessageRoleEnum::user(), $parts); } - - /** - * {@inheritDoc} - * - * @since n.e.x.t - */ - public static function fromJson(array $json): UserMessage - { - $partsData = $json['parts']; - $parts = array_map(function (array $partData) { - return MessagePart::fromJson($partData); - }, $partsData); - - return new self($parts); - } } diff --git a/src/Operations/Contracts/OperationInterface.php b/src/Operations/Contracts/OperationInterface.php index b5369b36..0ab0cdb9 100644 --- a/src/Operations/Contracts/OperationInterface.php +++ b/src/Operations/Contracts/OperationInterface.php @@ -16,7 +16,9 @@ * * @since n.e.x.t * - * @extends WithJsonSerialization> + * @template TJsonShape of array + * + * @extends WithJsonSerialization */ interface OperationInterface extends WithJsonSchemaInterface, WithJsonSerialization { diff --git a/src/Operations/DTO/GenerativeAiOperation.php b/src/Operations/DTO/GenerativeAiOperation.php index adda3b5d..ae37d049 100644 --- a/src/Operations/DTO/GenerativeAiOperation.php +++ b/src/Operations/DTO/GenerativeAiOperation.php @@ -20,9 +20,9 @@ * * @phpstan-type GenerativeAiOperationJsonShape array{id: string, state: string, result?: GenerativeAiResultJsonShape} * - * @implements WithJsonSerialization + * @implements OperationInterface */ -class GenerativeAiOperation implements OperationInterface +final class GenerativeAiOperation implements OperationInterface { /** * @var string Unique identifier for this operation. @@ -144,7 +144,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return GenerativeAiOperationJsonShape */ public function jsonSerialize(): array { @@ -170,8 +170,7 @@ public static function fromJson(array $json): GenerativeAiOperation $state = OperationStateEnum::from($json['state']); $result = null; if (isset($json['result'])) { - $resultData = $json['result']; - $result = GenerativeAiResult::fromJson($resultData); + $result = GenerativeAiResult::fromJson($json['result']); } return new self($json['id'], $state, $result); diff --git a/src/Results/Contracts/ResultInterface.php b/src/Results/Contracts/ResultInterface.php index 821dfab2..5a58b1d9 100644 --- a/src/Results/Contracts/ResultInterface.php +++ b/src/Results/Contracts/ResultInterface.php @@ -16,7 +16,9 @@ * * @since n.e.x.t * - * @extends WithJsonSerialization> + * @template TJsonShape of array + * + * @extends WithJsonSerialization */ interface ResultInterface extends WithJsonSchemaInterface, WithJsonSerialization { diff --git a/src/Results/DTO/Candidate.php b/src/Results/DTO/Candidate.php index 0d05606b..e8c13014 100644 --- a/src/Results/DTO/Candidate.php +++ b/src/Results/DTO/Candidate.php @@ -23,7 +23,7 @@ * * @implements WithJsonSerialization */ -class Candidate implements WithJsonSchemaInterface, WithJsonSerialization +final class Candidate implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var Message The generated message. @@ -128,7 +128,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return CandidateJsonShape */ public function jsonSerialize(): array { @@ -151,7 +151,7 @@ public static function fromJson(array $json): Candidate return new self( Message::fromJson($messageData), FinishReasonEnum::from($json['finishReason']), - $json['tokenCount'] + (int) $json['tokenCount'] ); } } diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index 6caaf0b0..191adf6b 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -27,9 +27,9 @@ * providerMetadata?: array * } * - * @implements WithJsonSerialization + * @implements ResultInterface */ -class GenerativeAiResult implements ResultInterface +final class GenerativeAiResult implements ResultInterface { /** * @var string Unique identifier for this result. @@ -396,7 +396,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return GenerativeAiResultJsonShape */ public function jsonSerialize(): array { @@ -417,22 +417,16 @@ public function jsonSerialize(): array */ public static function fromJson(array $json): GenerativeAiResult { - /** @var array $candidatesData */ $candidatesData = $json['candidates']; $candidates = array_map(function (array $candidateData) { return Candidate::fromJson($candidateData); }, $candidatesData); - /** @var TokenUsageJsonShape $tokenUsageData */ $tokenUsageData = $json['tokenUsage']; - /** @var array $providerMetadata */ $providerMetadata = $json['providerMetadata'] ?? []; - /** @var string $id */ - $id = $json['id']; - return new self( - $id, + $json['id'], $candidates, TokenUsage::fromJson($tokenUsageData), $providerMetadata diff --git a/src/Results/DTO/TokenUsage.php b/src/Results/DTO/TokenUsage.php index 95648f15..9d4375fc 100644 --- a/src/Results/DTO/TokenUsage.php +++ b/src/Results/DTO/TokenUsage.php @@ -23,7 +23,7 @@ * * @implements WithJsonSerialization */ -class TokenUsage implements WithJsonSchemaInterface, WithJsonSerialization +final class TokenUsage implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var int Number of tokens in the prompt. @@ -124,7 +124,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return TokenUsageJsonShape */ public function jsonSerialize(): array { diff --git a/src/Tools/DTO/FunctionCall.php b/src/Tools/DTO/FunctionCall.php index 3b263248..3915567b 100644 --- a/src/Tools/DTO/FunctionCall.php +++ b/src/Tools/DTO/FunctionCall.php @@ -19,7 +19,7 @@ * * @implements WithJsonSerialization */ -class FunctionCall implements WithJsonSchemaInterface, WithJsonSerialization +final class FunctionCall implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var string|null Unique identifier for this function call. @@ -136,7 +136,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return FunctionCallJsonShape */ public function jsonSerialize(): array { diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index b96fec1d..9afe57c0 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -19,7 +19,7 @@ * * @implements WithJsonSerialization */ -class FunctionDeclaration implements WithJsonSchemaInterface, WithJsonSerialization +final class FunctionDeclaration implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var string The name of the function. @@ -120,7 +120,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return FunctionDeclarationJsonShape */ public function jsonSerialize(): array { diff --git a/src/Tools/DTO/FunctionResponse.php b/src/Tools/DTO/FunctionResponse.php index c4425d00..e8d985f2 100644 --- a/src/Tools/DTO/FunctionResponse.php +++ b/src/Tools/DTO/FunctionResponse.php @@ -19,7 +19,7 @@ * * @implements WithJsonSerialization */ -class FunctionResponse implements WithJsonSchemaInterface, WithJsonSerialization +final class FunctionResponse implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var string The ID of the function call this is responding to. @@ -120,7 +120,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return FunctionResponseJsonShape */ public function jsonSerialize(): array { diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index 9cc100d1..6c0e76e5 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -27,7 +27,7 @@ * * @implements WithJsonSerialization */ -class Tool implements WithJsonSchemaInterface, WithJsonSerialization +final class Tool implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var ToolTypeEnum The type of tool. @@ -150,7 +150,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return ToolJsonShape */ public function jsonSerialize(): array { diff --git a/src/Tools/DTO/WebSearch.php b/src/Tools/DTO/WebSearch.php index d123feba..0e962bd3 100644 --- a/src/Tools/DTO/WebSearch.php +++ b/src/Tools/DTO/WebSearch.php @@ -19,7 +19,7 @@ * * @implements WithJsonSerialization */ -class WebSearch implements WithJsonSchemaInterface, WithJsonSerialization +final class WebSearch implements WithJsonSchemaInterface, WithJsonSerialization { /** * @var string[] List of domains that are allowed for web search. @@ -103,7 +103,7 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return array + * @return WebSearchJsonShape */ public function jsonSerialize(): array { From 404699abf04ac607eb7715c4184b16ec16552378 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 11:11:11 -0600 Subject: [PATCH 08/36] refactor: improves Message::toJson types --- src/Messages/DTO/Message.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index 144acb8a..3e14dcd6 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -122,6 +122,8 @@ public function jsonSerialize(): array * {@inheritDoc} * * @since n.e.x.t + * + * @return self|UserMessage|ModelMessage|SystemMessage */ final public static function fromJson(array $json): Message { @@ -133,17 +135,13 @@ final public static function fromJson(array $json): Message // Determine which concrete class to instantiate based on role if ($role->isUser()) { - /** @phpstan-ignore-next-line */ return new UserMessage($parts); } elseif ($role->isModel()) { - /** @phpstan-ignore-next-line */ return new ModelMessage($parts); } elseif ($role->isSystem()) { - /** @phpstan-ignore-next-line */ return new SystemMessage($parts); } - /** @phpstan-ignore-next-line */ return new self($role, $parts); } } From 18ffdb4f2417d5ab2fb18e545fec5cf27dbc7431 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 13:18:42 -0600 Subject: [PATCH 09/36] refactor: changes to array terminology over json --- .../WithArrayTransformationInterface.php | 34 +++++++ .../Contracts/WithJsonSerialization.php | 27 ------ src/Files/DTO/File.php | 26 +++--- src/Messages/DTO/Message.php | 26 +++--- src/Messages/DTO/MessagePart.php | 58 ++++++------ .../Contracts/OperationInterface.php | 8 +- src/Operations/DTO/GenerativeAiOperation.php | 22 ++--- src/Results/Contracts/ResultInterface.php | 8 +- src/Results/DTO/Candidate.php | 26 +++--- src/Results/DTO/GenerativeAiResult.php | 34 +++---- src/Results/DTO/TokenUsage.php | 20 ++-- src/Tools/DTO/FunctionCall.php | 20 ++-- src/Tools/DTO/FunctionDeclaration.php | 20 ++-- src/Tools/DTO/FunctionResponse.php | 20 ++-- src/Tools/DTO/Tool.php | 42 ++++----- src/Tools/DTO/WebSearch.php | 18 ++-- tests/traits/ArrayTransformationTestTrait.php | 88 ++++++++++++++++++ tests/traits/JsonSerializationTestTrait.php | 93 ------------------- tests/unit/Files/DTO/FileTest.php | 43 ++++----- tests/unit/Messages/DTO/MessagePartTest.php | 47 +++++----- tests/unit/Messages/DTO/MessageTest.php | 29 +++--- tests/unit/Messages/DTO/ModelMessageTest.php | 28 +++--- tests/unit/Messages/DTO/SystemMessageTest.php | 28 +++--- tests/unit/Messages/DTO/UserMessageTest.php | 28 +++--- .../DTO/GenerativeAiOperationTest.php | 48 +++++----- tests/unit/Results/DTO/CandidateTest.php | 28 +++--- .../Results/DTO/GenerativeAiResultTest.php | 36 +++---- tests/unit/Results/DTO/TokenUsageTest.php | 29 +++--- tests/unit/Tools/DTO/FunctionCallTest.php | 45 +++++---- .../Tools/DTO/FunctionDeclarationTest.php | 40 ++++---- tests/unit/Tools/DTO/FunctionResponseTest.php | 28 +++--- tests/unit/Tools/DTO/ToolTest.php | 46 ++++----- tests/unit/Tools/DTO/WebSearchTest.php | 56 +++++------ 33 files changed, 568 insertions(+), 581 deletions(-) create mode 100644 src/Common/Contracts/WithArrayTransformationInterface.php delete mode 100644 src/Common/Contracts/WithJsonSerialization.php create mode 100644 tests/traits/ArrayTransformationTestTrait.php delete mode 100644 tests/traits/JsonSerializationTestTrait.php diff --git a/src/Common/Contracts/WithArrayTransformationInterface.php b/src/Common/Contracts/WithArrayTransformationInterface.php new file mode 100644 index 00000000..c0b3ffeb --- /dev/null +++ b/src/Common/Contracts/WithArrayTransformationInterface.php @@ -0,0 +1,34 @@ + + */ +interface WithArrayTransformationInterface +{ + /** + * Converts the object to an array representation. + * + * @since 1.0.0 + * + * @return TArrayShape The array representation. + */ + public function toArray(): array; + + /** + * Creates an instance from array data. + * + * @since 1.0.0 + * + * @param TArrayShape $array The array data. + * @return static The created instance. + */ + public static function fromArray(array $array); +} diff --git a/src/Common/Contracts/WithJsonSerialization.php b/src/Common/Contracts/WithJsonSerialization.php deleted file mode 100644 index 4e5b857b..00000000 --- a/src/Common/Contracts/WithJsonSerialization.php +++ /dev/null @@ -1,27 +0,0 @@ - - */ -interface WithJsonSerialization extends JsonSerializable -{ - /** - * Creates an instance from JSON data. - * - * @since 1.0.0 - * - * @param TJsonShape $json The JSON data. - * @return static The created instance. - */ - public static function fromJson(array $json); -} diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index e97c9818..0599cf5e 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Files\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; use WordPress\AiClient\Files\Enums\FileTypeEnum; use WordPress\AiClient\Files\ValueObjects\MimeType; @@ -17,16 +17,16 @@ * * @since n.e.x.t * - * @phpstan-type FileJsonShape array{ + * @phpstan-type FileArrayShape array{ * fileType: string, * url?: string, * mimeType?: string, * base64Data?: string * } * - * @implements WithJsonSerialization + * @implements WithArrayTransformationInterface */ -final class File implements WithJsonSchemaInterface, WithJsonSerialization +final class File implements WithJsonSchemaInterface, WithArrayTransformationInterface { /** * @var MimeType The MIME type of the file. @@ -393,9 +393,9 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return FileJsonShape + * @return FileArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { $data = [ 'fileType' => $this->fileType->value, @@ -416,22 +416,22 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): File + public static function fromArray(array $array): File { - $fileType = FileTypeEnum::from($json['fileType']); + $fileType = FileTypeEnum::from($array['fileType']); if ($fileType->isRemote()) { - if (!isset($json['url']) || !isset($json['mimeType'])) { + if (!isset($array['url']) || !isset($array['mimeType'])) { throw new \InvalidArgumentException('Remote file requires url and mimeType.'); } - return new self($json['url'], $json['mimeType']); + return new self($array['url'], $array['mimeType']); } else { - if (!isset($json['mimeType']) || !isset($json['base64Data'])) { + if (!isset($array['mimeType']) || !isset($array['base64Data'])) { throw new \InvalidArgumentException('Inline file requires mimeType and base64Data.'); } // Create data URI from base64 data and mime type - $mimeType = $json['mimeType']; - $base64Data = $json['base64Data']; + $mimeType = $array['mimeType']; + $base64Data = $array['base64Data']; $dataUri = sprintf('data:%s;base64,%s', $mimeType, $base64Data); return new self($dataUri); } diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index 3e14dcd6..fe29c951 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Messages\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; /** @@ -16,16 +16,16 @@ * * @since n.e.x.t * - * @phpstan-import-type MessagePartJsonShape from MessagePart + * @phpstan-import-type MessagePartArrayShape from MessagePart * - * @phpstan-type MessageJsonShape array{ + * @phpstan-type MessageArrayShape array{ * role: string, - * parts: array + * parts: array * } * - * @implements WithJsonSerialization + * @implements WithArrayTransformationInterface */ -class Message implements WithJsonSchemaInterface, WithJsonSerialization +class Message implements WithJsonSchemaInterface, WithArrayTransformationInterface { /** * @var MessageRoleEnum The role of the message sender. @@ -106,14 +106,14 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return MessageJsonShape + * @return MessageArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { return [ 'role' => $this->role->value, 'parts' => array_map(function (MessagePart $part) { - return $part->jsonSerialize(); + return $part->toArray(); }, $this->parts), ]; } @@ -125,12 +125,12 @@ public function jsonSerialize(): array * * @return self|UserMessage|ModelMessage|SystemMessage */ - final public static function fromJson(array $json): Message + final public static function fromArray(array $array): Message { - $role = MessageRoleEnum::from($json['role']); - $partsData = $json['parts']; + $role = MessageRoleEnum::from($array['role']); + $partsData = $array['parts']; $parts = array_map(function (array $partData) { - return MessagePart::fromJson($partData); + return MessagePart::fromArray($partData); }, $partsData); // Determine which concrete class to instantiate based on role diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index d8d9c299..e8bde310 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Messages\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Tools\DTO\FunctionCall; @@ -19,21 +19,21 @@ * * @since n.e.x.t * - * @phpstan-import-type FileJsonShape from File - * @phpstan-import-type FunctionCallJsonShape from FunctionCall - * @phpstan-import-type FunctionResponseJsonShape from FunctionResponse + * @phpstan-import-type FileArrayShape from File + * @phpstan-import-type FunctionCallArrayShape from FunctionCall + * @phpstan-import-type FunctionResponseArrayShape from FunctionResponse * - * @phpstan-type MessagePartJsonShape array{ + * @phpstan-type MessagePartArrayShape array{ * type: string, * text?: string, - * file?: FileJsonShape, - * functionCall?: FunctionCallJsonShape, - * functionResponse?: FunctionResponseJsonShape + * file?: FileArrayShape, + * functionCall?: FunctionCallArrayShape, + * functionResponse?: FunctionResponseArrayShape * } * - * @implements WithJsonSerialization + * @implements WithArrayTransformationInterface */ -final class MessagePart implements WithJsonSchemaInterface, WithJsonSerialization +final class MessagePart implements WithJsonSchemaInterface, WithArrayTransformationInterface { /** * @var MessagePartTypeEnum The type of this message part. @@ -223,20 +223,20 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return MessagePartJsonShape + * @return MessagePartArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { $data = ['type' => $this->type->value]; if ($this->type->isText() && $this->text !== null) { $data['text'] = $this->text; } elseif ($this->type->isFile() && $this->file !== null) { - $data['file'] = $this->file->jsonSerialize(); + $data['file'] = $this->file->toArray(); } elseif ($this->type->isFunctionCall() && $this->functionCall !== null) { - $data['functionCall'] = $this->functionCall->jsonSerialize(); + $data['functionCall'] = $this->functionCall->toArray(); } elseif ($this->type->isFunctionResponse() && $this->functionResponse !== null) { - $data['functionResponse'] = $this->functionResponse->jsonSerialize(); + $data['functionResponse'] = $this->functionResponse->toArray(); } return $data; @@ -247,35 +247,35 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): MessagePart + public static function fromArray(array $array): MessagePart { - $type = MessagePartTypeEnum::from($json['type']); + $type = MessagePartTypeEnum::from($array['type']); if ($type->isText()) { - if (!isset($json['text'])) { + if (!isset($array['text'])) { throw new \InvalidArgumentException('Text message part requires text field.'); } - return new self($json['text']); + return new self($array['text']); } elseif ($type->isFile()) { - if (!isset($json['file'])) { + if (!isset($array['file'])) { throw new \InvalidArgumentException('File message part requires file field.'); } - $fileData = $json['file']; - return new self(File::fromJson($fileData)); + $fileData = $array['file']; + return new self(File::fromArray($fileData)); } elseif ($type->isFunctionCall()) { - if (!isset($json['functionCall'])) { + if (!isset($array['functionCall'])) { throw new \InvalidArgumentException('Function call message part requires functionCall field.'); } - $functionCallData = $json['functionCall']; - return new self(FunctionCall::fromJson($functionCallData)); + $functionCallData = $array['functionCall']; + return new self(FunctionCall::fromArray($functionCallData)); } elseif ($type->isFunctionResponse()) { - if (!isset($json['functionResponse'])) { + if (!isset($array['functionResponse'])) { throw new \InvalidArgumentException('Function response message part requires functionResponse field.'); } - $functionResponseData = $json['functionResponse']; - return new self(FunctionResponse::fromJson($functionResponseData)); + $functionResponseData = $array['functionResponse']; + return new self(FunctionResponse::fromArray($functionResponseData)); } - throw new \InvalidArgumentException(sprintf('Unknown message part type: %s', $json['type'])); + throw new \InvalidArgumentException(sprintf('Unknown message part type: %s', $array['type'])); } } diff --git a/src/Operations/Contracts/OperationInterface.php b/src/Operations/Contracts/OperationInterface.php index 0ab0cdb9..1451d561 100644 --- a/src/Operations/Contracts/OperationInterface.php +++ b/src/Operations/Contracts/OperationInterface.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Operations\Contracts; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; use WordPress\AiClient\Operations\Enums\OperationStateEnum; /** @@ -16,11 +16,11 @@ * * @since n.e.x.t * - * @template TJsonShape of array + * @template TArrayShape of array * - * @extends WithJsonSerialization + * @extends WithArrayTransformationInterface */ -interface OperationInterface extends WithJsonSchemaInterface, WithJsonSerialization +interface OperationInterface extends WithJsonSchemaInterface, WithArrayTransformationInterface { /** * Gets the operation ID. diff --git a/src/Operations/DTO/GenerativeAiOperation.php b/src/Operations/DTO/GenerativeAiOperation.php index ae37d049..f3287f11 100644 --- a/src/Operations/DTO/GenerativeAiOperation.php +++ b/src/Operations/DTO/GenerativeAiOperation.php @@ -16,11 +16,11 @@ * * @since n.e.x.t * - * @phpstan-import-type GenerativeAiResultJsonShape from GenerativeAiResult + * @phpstan-import-type GenerativeAiResultArrayShape from GenerativeAiResult * - * @phpstan-type GenerativeAiOperationJsonShape array{id: string, state: string, result?: GenerativeAiResultJsonShape} + * @phpstan-type GenerativeAiOperationArrayShape array{id: string, state: string, result?: GenerativeAiResultArrayShape} * - * @implements OperationInterface + * @implements OperationInterface */ final class GenerativeAiOperation implements OperationInterface { @@ -144,9 +144,9 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return GenerativeAiOperationJsonShape + * @return GenerativeAiOperationArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { $data = [ 'id' => $this->id, @@ -154,7 +154,7 @@ public function jsonSerialize(): array ]; if ($this->result !== null) { - $data['result'] = $this->result->jsonSerialize(); + $data['result'] = $this->result->toArray(); } return $data; @@ -165,14 +165,14 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): GenerativeAiOperation + public static function fromArray(array $array): GenerativeAiOperation { - $state = OperationStateEnum::from($json['state']); + $state = OperationStateEnum::from($array['state']); $result = null; - if (isset($json['result'])) { - $result = GenerativeAiResult::fromJson($json['result']); + if (isset($array['result'])) { + $result = GenerativeAiResult::fromArray($array['result']); } - return new self($json['id'], $state, $result); + return new self($array['id'], $state, $result); } } diff --git a/src/Results/Contracts/ResultInterface.php b/src/Results/Contracts/ResultInterface.php index 5a58b1d9..5caa5762 100644 --- a/src/Results/Contracts/ResultInterface.php +++ b/src/Results/Contracts/ResultInterface.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Results\Contracts; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; use WordPress\AiClient\Results\DTO\TokenUsage; /** @@ -16,11 +16,11 @@ * * @since n.e.x.t * - * @template TJsonShape of array + * @template TArrayShape of array * - * @extends WithJsonSerialization + * @extends WithArrayTransformationInterface */ -interface ResultInterface extends WithJsonSchemaInterface, WithJsonSerialization +interface ResultInterface extends WithJsonSchemaInterface, WithArrayTransformationInterface { /** * Gets the result ID. diff --git a/src/Results/DTO/Candidate.php b/src/Results/DTO/Candidate.php index e8c13014..c05db20a 100644 --- a/src/Results/DTO/Candidate.php +++ b/src/Results/DTO/Candidate.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Results\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; use WordPress\AiClient\Messages\DTO\Message; use WordPress\AiClient\Results\Enums\FinishReasonEnum; @@ -17,13 +17,13 @@ * * @since n.e.x.t * - * @phpstan-import-type MessageJsonShape from Message + * @phpstan-import-type MessageArrayShape from Message * - * @phpstan-type CandidateJsonShape array{message: MessageJsonShape, finishReason: string, tokenCount: int|string} + * @phpstan-type CandidateArrayShape array{message: MessageArrayShape, finishReason: string, tokenCount: int|string} * - * @implements WithJsonSerialization + * @implements WithArrayTransformationInterface */ -final class Candidate implements WithJsonSchemaInterface, WithJsonSerialization +final class Candidate implements WithJsonSchemaInterface, WithArrayTransformationInterface { /** * @var Message The generated message. @@ -128,12 +128,12 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return CandidateJsonShape + * @return CandidateArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { return [ - 'message' => $this->message->jsonSerialize(), + 'message' => $this->message->toArray(), 'finishReason' => $this->finishReason->value, 'tokenCount' => $this->tokenCount, ]; @@ -144,14 +144,14 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): Candidate + public static function fromArray(array $array): Candidate { - $messageData = $json['message']; + $messageData = $array['message']; return new self( - Message::fromJson($messageData), - FinishReasonEnum::from($json['finishReason']), - (int) $json['tokenCount'] + Message::fromArray($messageData), + FinishReasonEnum::from($array['finishReason']), + (int) $array['tokenCount'] ); } } diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index 191adf6b..35a619aa 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -17,17 +17,17 @@ * * @since n.e.x.t * - * @phpstan-import-type CandidateJsonShape from Candidate - * @phpstan-import-type TokenUsageJsonShape from TokenUsage + * @phpstan-import-type CandidateArrayShape from Candidate + * @phpstan-import-type TokenUsageArrayShape from TokenUsage * - * @phpstan-type GenerativeAiResultJsonShape array{ + * @phpstan-type GenerativeAiResultArrayShape array{ * id: string, - * candidates: array, - * tokenUsage: TokenUsageJsonShape, + * candidates: array, + * tokenUsage: TokenUsageArrayShape, * providerMetadata?: array * } * - * @implements ResultInterface + * @implements ResultInterface */ final class GenerativeAiResult implements ResultInterface { @@ -396,16 +396,16 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return GenerativeAiResultJsonShape + * @return GenerativeAiResultArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { return [ 'id' => $this->id, 'candidates' => array_map(function (Candidate $candidate) { - return $candidate->jsonSerialize(); + return $candidate->toArray(); }, $this->candidates), - 'tokenUsage' => $this->tokenUsage->jsonSerialize(), + 'tokenUsage' => $this->tokenUsage->toArray(), 'providerMetadata' => $this->providerMetadata, ]; } @@ -415,20 +415,20 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): GenerativeAiResult + public static function fromArray(array $array): GenerativeAiResult { - $candidatesData = $json['candidates']; + $candidatesData = $array['candidates']; $candidates = array_map(function (array $candidateData) { - return Candidate::fromJson($candidateData); + return Candidate::fromArray($candidateData); }, $candidatesData); - $tokenUsageData = $json['tokenUsage']; - $providerMetadata = $json['providerMetadata'] ?? []; + $tokenUsageData = $array['tokenUsage']; + $providerMetadata = $array['providerMetadata'] ?? []; return new self( - $json['id'], + $array['id'], $candidates, - TokenUsage::fromJson($tokenUsageData), + TokenUsage::fromArray($tokenUsageData), $providerMetadata ); } diff --git a/src/Results/DTO/TokenUsage.php b/src/Results/DTO/TokenUsage.php index 9d4375fc..faf9ac45 100644 --- a/src/Results/DTO/TokenUsage.php +++ b/src/Results/DTO/TokenUsage.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Results\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; /** * Represents token usage statistics for an AI operation. @@ -15,15 +15,15 @@ * * @since n.e.x.t * - * @phpstan-type TokenUsageJsonShape array{ + * @phpstan-type TokenUsageArrayShape array{ * promptTokens: int|string, * completionTokens: int|string, * totalTokens: int|string * } * - * @implements WithJsonSerialization + * @implements WithArrayTransformationInterface */ -final class TokenUsage implements WithJsonSchemaInterface, WithJsonSerialization +final class TokenUsage implements WithJsonSchemaInterface, WithArrayTransformationInterface { /** * @var int Number of tokens in the prompt. @@ -124,9 +124,9 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return TokenUsageJsonShape + * @return TokenUsageArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { return [ 'promptTokens' => $this->promptTokens, @@ -140,12 +140,12 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): TokenUsage + public static function fromArray(array $array): TokenUsage { return new self( - (int) $json['promptTokens'], - (int) $json['completionTokens'], - (int) $json['totalTokens'] + (int) $array['promptTokens'], + (int) $array['completionTokens'], + (int) $array['totalTokens'] ); } } diff --git a/src/Tools/DTO/FunctionCall.php b/src/Tools/DTO/FunctionCall.php index 3915567b..5d40fb84 100644 --- a/src/Tools/DTO/FunctionCall.php +++ b/src/Tools/DTO/FunctionCall.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Tools\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; /** * Represents a function call request from an AI model. @@ -15,11 +15,11 @@ * * @since n.e.x.t * - * @phpstan-type FunctionCallJsonShape array{id?: string, name?: string, args?: mixed} + * @phpstan-type FunctionCallArrayShape array{id?: string, name?: string, args?: mixed} * - * @implements WithJsonSerialization + * @implements WithArrayTransformationInterface */ -final class FunctionCall implements WithJsonSchemaInterface, WithJsonSerialization +final class FunctionCall implements WithJsonSchemaInterface, WithArrayTransformationInterface { /** * @var string|null Unique identifier for this function call. @@ -136,9 +136,9 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return FunctionCallJsonShape + * @return FunctionCallArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { $data = []; @@ -162,14 +162,14 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): FunctionCall + public static function fromArray(array $array): FunctionCall { /** @var array $args */ - $args = $json['args'] ?? []; + $args = $array['args'] ?? []; return new self( - isset($json['id']) ? $json['id'] : null, - isset($json['name']) ? $json['name'] : null, + isset($array['id']) ? $array['id'] : null, + isset($array['name']) ? $array['name'] : null, $args ); } diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index 9afe57c0..02631b2a 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Tools\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; /** * Represents a function declaration for AI models. @@ -15,11 +15,11 @@ * * @since n.e.x.t * - * @phpstan-type FunctionDeclarationJsonShape array{name: string, description: string, parameters?: mixed} + * @phpstan-type FunctionDeclarationArrayShape array{name: string, description: string, parameters?: mixed} * - * @implements WithJsonSerialization + * @implements WithArrayTransformationInterface */ -final class FunctionDeclaration implements WithJsonSchemaInterface, WithJsonSerialization +final class FunctionDeclaration implements WithJsonSchemaInterface, WithArrayTransformationInterface { /** * @var string The name of the function. @@ -120,9 +120,9 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return FunctionDeclarationJsonShape + * @return FunctionDeclarationArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { $data = [ 'name' => $this->name, @@ -141,12 +141,12 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): FunctionDeclaration + public static function fromArray(array $array): FunctionDeclaration { return new self( - $json['name'], - $json['description'], - $json['parameters'] ?? null + $array['name'], + $array['description'], + $array['parameters'] ?? null ); } } diff --git a/src/Tools/DTO/FunctionResponse.php b/src/Tools/DTO/FunctionResponse.php index e8d985f2..686de175 100644 --- a/src/Tools/DTO/FunctionResponse.php +++ b/src/Tools/DTO/FunctionResponse.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Tools\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; /** * Represents a response to a function call. @@ -15,11 +15,11 @@ * * @since n.e.x.t * - * @phpstan-type FunctionResponseJsonShape array{id: string, name: string, response: mixed} + * @phpstan-type FunctionResponseArrayShape array{id: string, name: string, response: mixed} * - * @implements WithJsonSerialization + * @implements WithArrayTransformationInterface */ -final class FunctionResponse implements WithJsonSchemaInterface, WithJsonSerialization +final class FunctionResponse implements WithJsonSchemaInterface, WithArrayTransformationInterface { /** * @var string The ID of the function call this is responding to. @@ -120,9 +120,9 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return FunctionResponseJsonShape + * @return FunctionResponseArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { return [ 'id' => $this->id, @@ -136,12 +136,12 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): FunctionResponse + public static function fromArray(array $array): FunctionResponse { return new self( - $json['id'], - $json['name'], - $json['response'] + $array['id'], + $array['name'], + $array['response'] ); } } diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index 6c0e76e5..41fc020b 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Tools\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; use WordPress\AiClient\Providers\Enums\ToolTypeEnum; /** @@ -16,18 +16,18 @@ * * @since n.e.x.t * - * @phpstan-import-type FunctionDeclarationJsonShape from FunctionDeclaration - * @phpstan-import-type WebSearchJsonShape from WebSearch + * @phpstan-import-type FunctionDeclarationArrayShape from FunctionDeclaration + * @phpstan-import-type WebSearchArrayShape from WebSearch * - * @phpstan-type ToolJsonShape array{ + * @phpstan-type ToolArrayShape array{ * type: string, - * functionDeclarations?: array, - * webSearch?: WebSearchJsonShape + * functionDeclarations?: array, + * webSearch?: WebSearchArrayShape * } * - * @implements WithJsonSerialization + * @implements WithArrayTransformationInterface */ -final class Tool implements WithJsonSchemaInterface, WithJsonSerialization +final class Tool implements WithJsonSchemaInterface, WithArrayTransformationInterface { /** * @var ToolTypeEnum The type of tool. @@ -150,18 +150,18 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return ToolJsonShape + * @return ToolArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { $data = ['type' => $this->type->value]; if ($this->type->isFunctionDeclarations() && $this->functionDeclarations !== null) { $data['functionDeclarations'] = array_map(function (FunctionDeclaration $declaration) { - return $declaration->jsonSerialize(); + return $declaration->toArray(); }, $this->functionDeclarations); } elseif ($this->type->isWebSearch() && $this->webSearch !== null) { - $data['webSearch'] = $this->webSearch->jsonSerialize(); + $data['webSearch'] = $this->webSearch->toArray(); } return $data; @@ -172,27 +172,27 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): Tool + public static function fromArray(array $array): Tool { - $type = ToolTypeEnum::from($json['type']); + $type = ToolTypeEnum::from($array['type']); if ($type->isFunctionDeclarations()) { - if (!isset($json['functionDeclarations'])) { + if (!isset($array['functionDeclarations'])) { throw new \InvalidArgumentException('Function declarations tool requires functionDeclarations field.'); } - $declarationsData = $json['functionDeclarations']; + $declarationsData = $array['functionDeclarations']; $declarations = array_map(function (array $declarationData) { - return FunctionDeclaration::fromJson($declarationData); + return FunctionDeclaration::fromArray($declarationData); }, $declarationsData); return new self($declarations); } elseif ($type->isWebSearch()) { - if (!isset($json['webSearch'])) { + if (!isset($array['webSearch'])) { throw new \InvalidArgumentException('Web search tool requires webSearch field.'); } - $webSearchData = $json['webSearch']; - return new self(WebSearch::fromJson($webSearchData)); + $webSearchData = $array['webSearch']; + return new self(WebSearch::fromArray($webSearchData)); } - throw new \InvalidArgumentException(sprintf('Unknown tool type: %s', $json['type'])); + throw new \InvalidArgumentException(sprintf('Unknown tool type: %s', $array['type'])); } } diff --git a/src/Tools/DTO/WebSearch.php b/src/Tools/DTO/WebSearch.php index 0e962bd3..9b059c28 100644 --- a/src/Tools/DTO/WebSearch.php +++ b/src/Tools/DTO/WebSearch.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Tools\DTO; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithJsonSerialization; +use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; /** * Represents web search configuration for AI models. @@ -15,11 +15,11 @@ * * @since n.e.x.t * - * @phpstan-type WebSearchJsonShape array{allowedDomains?: string[], disallowedDomains?: string[]} + * @phpstan-type WebSearchArrayShape array{allowedDomains?: string[], disallowedDomains?: string[]} * - * @implements WithJsonSerialization + * @implements WithArrayTransformationInterface */ -final class WebSearch implements WithJsonSchemaInterface, WithJsonSerialization +final class WebSearch implements WithJsonSchemaInterface, WithArrayTransformationInterface { /** * @var string[] List of domains that are allowed for web search. @@ -103,9 +103,9 @@ public static function getJsonSchema(): array * * @since n.e.x.t * - * @return WebSearchJsonShape + * @return WebSearchArrayShape */ - public function jsonSerialize(): array + public function toArray(): array { return [ 'allowedDomains' => $this->allowedDomains, @@ -118,10 +118,10 @@ public function jsonSerialize(): array * * @since n.e.x.t */ - public static function fromJson(array $json): WebSearch + public static function fromArray(array $array): WebSearch { - $allowedDomains = $json['allowedDomains'] ?? []; - $disallowedDomains = $json['disallowedDomains'] ?? []; + $allowedDomains = $array['allowedDomains'] ?? []; + $disallowedDomains = $array['disallowedDomains'] ?? []; return new self( $allowedDomains, diff --git a/tests/traits/ArrayTransformationTestTrait.php b/tests/traits/ArrayTransformationTestTrait.php new file mode 100644 index 00000000..f32381f1 --- /dev/null +++ b/tests/traits/ArrayTransformationTestTrait.php @@ -0,0 +1,88 @@ +assertInstanceOf( + \WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface::class, + $object, + 'Object should implement WithArrayTransformationInterface interface' + ); + } + + /** + * Asserts that toArray returns a valid array. + * + * @param object $object The object to test. + * @return array The serialized data. + */ + protected function assertToArrayReturnsArray($object): array + { + $array = $object->toArray(); + $this->assertIsArray($array, 'toArray() should return an array'); + return $array; + } + + /** + * Asserts round-trip array transformation works correctly. + * + * @param object $original The original object. + * @param callable $assertCallback Callback to assert equality between original and restored. + * @return void + */ + protected function assertArrayRoundTrip($original, callable $assertCallback): void + { + $array = $original->toArray(); + $className = get_class($original); + $restored = $className::fromArray($array); + + $this->assertInstanceOf($className, $restored, 'fromArray() should return instance of ' . $className); + $assertCallback($original, $restored); + } + + /** + * Asserts that specific keys exist in transformed array. + * + * @param array $array The transformed array. + * @param array $expectedKeys The keys that should exist. + * @return void + */ + protected function assertArrayHasKeys(array $array, array $expectedKeys): void + { + foreach ($expectedKeys as $key) { + $this->assertArrayHasKey($key, $array, "Array should contain key: {$key}"); + } + } + + /** + * Asserts that specific keys do not exist in transformed array. + * + * @param array $array The transformed array. + * @param array $unexpectedKeys The keys that should not exist. + * @return void + */ + protected function assertArrayNotHasKeys(array $array, array $unexpectedKeys): void + { + foreach ($unexpectedKeys as $key) { + $this->assertArrayNotHasKey($key, $array, "Array should not contain key: {$key}"); + } + } +} \ No newline at end of file diff --git a/tests/traits/JsonSerializationTestTrait.php b/tests/traits/JsonSerializationTestTrait.php deleted file mode 100644 index de3d83c8..00000000 --- a/tests/traits/JsonSerializationTestTrait.php +++ /dev/null @@ -1,93 +0,0 @@ -assertInstanceOf( - \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, - $object, - 'Object should implement WithJsonSerialization interface' - ); - $this->assertInstanceOf( - \JsonSerializable::class, - $object, - 'Object should implement JsonSerializable interface' - ); - } - - /** - * Asserts that jsonSerialize returns a valid array. - * - * @param object $object The object to test. - * @return array The serialized data. - */ - protected function assertJsonSerializeReturnsArray($object): array - { - $json = $object->jsonSerialize(); - $this->assertIsArray($json, 'jsonSerialize() should return an array'); - return $json; - } - - /** - * Asserts round-trip JSON serialization works correctly. - * - * @param object $original The original object. - * @param callable $assertCallback Callback to assert equality between original and restored. - * @return void - */ - protected function assertJsonRoundTrip($original, callable $assertCallback): void - { - $json = $original->jsonSerialize(); - $className = get_class($original); - $restored = $className::fromJson($json); - - $this->assertInstanceOf($className, $restored, 'fromJson() should return instance of ' . $className); - $assertCallback($original, $restored); - } - - /** - * Asserts that specific keys exist in serialized JSON. - * - * @param array $json The serialized JSON array. - * @param array $expectedKeys The keys that should exist. - * @return void - */ - protected function assertJsonHasKeys(array $json, array $expectedKeys): void - { - foreach ($expectedKeys as $key) { - $this->assertArrayHasKey($key, $json, "JSON should contain key: {$key}"); - } - } - - /** - * Asserts that specific keys do not exist in serialized JSON. - * - * @param array $json The serialized JSON array. - * @param array $unexpectedKeys The keys that should not exist. - * @return void - */ - protected function assertJsonNotHasKeys(array $json, array $unexpectedKeys): void - { - foreach ($unexpectedKeys as $key) { - $this->assertArrayNotHasKey($key, $json, "JSON should not contain key: {$key}"); - } - } -} \ No newline at end of file diff --git a/tests/unit/Files/DTO/FileTest.php b/tests/unit/Files/DTO/FileTest.php index e9e5c128..73089a6d 100644 --- a/tests/unit/Files/DTO/FileTest.php +++ b/tests/unit/Files/DTO/FileTest.php @@ -269,14 +269,14 @@ public function testUrlWithUnknownExtension(): void } /** - * Tests JSON serialization for remote file. + * Tests array transformation for remote file. * * @return void */ - public function testJsonSerializeRemoteFile(): void + public function testToArrayRemoteFile(): void { $file = new File('https://example.com/image.jpg', 'image/jpeg'); - $json = $file->jsonSerialize(); + $json = $file->toArray(); $this->assertIsArray($json); $this->assertEquals(\WordPress\AiClient\Files\Enums\FileTypeEnum::remote()->value, $json['fileType']); @@ -286,16 +286,16 @@ public function testJsonSerializeRemoteFile(): void } /** - * Tests JSON serialization for inline file. + * Tests array transformation for inline file. * * @return void */ - public function testJsonSerializeInlineFile(): void + public function testToArrayInlineFile(): void { $base64Data = 'SGVsbG8gV29ybGQ='; $dataUri = 'data:text/plain;base64,' . $base64Data; $file = new File($dataUri); - $json = $file->jsonSerialize(); + $json = $file->toArray(); $this->assertIsArray($json); $this->assertEquals(\WordPress\AiClient\Files\Enums\FileTypeEnum::inline()->value, $json['fileType']); @@ -309,7 +309,7 @@ public function testJsonSerializeInlineFile(): void * * @return void */ - public function testFromJsonRemoteFile(): void + public function testFromArrayRemoteFile(): void { $json = [ 'fileType' => \WordPress\AiClient\Files\Enums\FileTypeEnum::remote()->value, @@ -317,7 +317,7 @@ public function testFromJsonRemoteFile(): void 'url' => 'https://example.com/test.png' ]; - $file = File::fromJson($json); + $file = File::fromArray($json); $this->assertInstanceOf(File::class, $file); $this->assertTrue($file->getFileType()->isRemote()); @@ -331,7 +331,7 @@ public function testFromJsonRemoteFile(): void * * @return void */ - public function testFromJsonInlineFile(): void + public function testFromArrayInlineFile(): void { $base64Data = 'SGVsbG8gV29ybGQ='; $json = [ @@ -340,7 +340,7 @@ public function testFromJsonInlineFile(): void 'base64Data' => $base64Data ]; - $file = File::fromJson($json); + $file = File::fromArray($json); $this->assertInstanceOf(File::class, $file); $this->assertTrue($file->getFileType()->isInline()); @@ -350,16 +350,16 @@ public function testFromJsonInlineFile(): void } /** - * Tests round-trip JSON serialization. + * Tests round-trip array transformation. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { // Test remote file $remoteFile = new File('https://example.com/doc.pdf', 'application/pdf'); - $remoteJson = $remoteFile->jsonSerialize(); - $restoredRemote = File::fromJson($remoteJson); + $remoteJson = $remoteFile->toArray(); + $restoredRemote = File::fromArray($remoteJson); $this->assertEquals($remoteFile->getFileType()->value, $restoredRemote->getFileType()->value); $this->assertEquals($remoteFile->getMimeType(), $restoredRemote->getMimeType()); @@ -368,8 +368,8 @@ public function testJsonRoundTrip(): void // Test inline file $dataUri = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; $inlineFile = new File($dataUri); - $inlineJson = $inlineFile->jsonSerialize(); - $restoredInline = File::fromJson($inlineJson); + $inlineJson = $inlineFile->toArray(); + $restoredInline = File::fromArray($inlineJson); $this->assertEquals($inlineFile->getFileType()->value, $restoredInline->getFileType()->value); $this->assertEquals($inlineFile->getMimeType(), $restoredInline->getMimeType()); @@ -377,21 +377,18 @@ public function testJsonRoundTrip(): void } /** - * Tests File implements WithJsonSerialization. + * Tests File implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $file = new File('https://example.com/test.jpg'); $this->assertInstanceOf( - \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, - $file - ); - $this->assertInstanceOf( - \JsonSerializable::class, + \WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface::class, $file ); + } } \ No newline at end of file diff --git a/tests/unit/Messages/DTO/MessagePartTest.php b/tests/unit/Messages/DTO/MessagePartTest.php index d49fcdea..08078014 100644 --- a/tests/unit/Messages/DTO/MessagePartTest.php +++ b/tests/unit/Messages/DTO/MessagePartTest.php @@ -233,14 +233,14 @@ public function testWithUnicodeText(): void } /** - * Tests JSON serialization with text content. + * Tests array transformation with text content. * * @return void */ - public function testJsonSerializeWithText(): void + public function testToArrayWithText(): void { $part = new MessagePart('Hello, world!'); - $json = $part->jsonSerialize(); + $json = $part->toArray(); $this->assertIsArray($json); $this->assertArrayHasKey('type', $json); @@ -255,15 +255,15 @@ public function testJsonSerializeWithText(): void } /** - * Tests JSON serialization with file content. + * Tests array transformation with file content. * * @return void */ - public function testJsonSerializeWithFile(): void + public function testToArrayWithFile(): void { $file = new File('https://example.com/image.jpg', 'image/jpeg'); $part = new MessagePart($file); - $json = $part->jsonSerialize(); + $json = $part->toArray(); $this->assertIsArray($json); $this->assertArrayHasKey('type', $json); @@ -277,14 +277,14 @@ public function testJsonSerializeWithFile(): void * * @return void */ - public function testFromJsonWithText(): void + public function testFromArrayWithText(): void { $json = [ 'type' => MessagePartTypeEnum::text()->value, 'text' => 'Test message' ]; - $part = MessagePart::fromJson($json); + $part = MessagePart::fromArray($json); $this->assertEquals(MessagePartTypeEnum::text(), $part->getType()); $this->assertEquals('Test message', $part->getText()); @@ -295,7 +295,7 @@ public function testFromJsonWithText(): void * * @return void */ - public function testFromJsonWithFile(): void + public function testFromArrayWithFile(): void { $json = [ 'type' => MessagePartTypeEnum::file()->value, @@ -306,7 +306,7 @@ public function testFromJsonWithFile(): void ] ]; - $part = MessagePart::fromJson($json); + $part = MessagePart::fromArray($json); $this->assertEquals(MessagePartTypeEnum::file(), $part->getType()); $this->assertInstanceOf(File::class, $part->getFile()); @@ -314,52 +314,49 @@ public function testFromJsonWithFile(): void } /** - * Tests round-trip JSON serialization with different content types. + * Tests round-trip array transformation with different content types. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { // Test with text $textPart = new MessagePart('Test text'); - $textJson = $textPart->jsonSerialize(); - $restoredText = MessagePart::fromJson($textJson); + $textJson = $textPart->toArray(); + $restoredText = MessagePart::fromArray($textJson); $this->assertEquals($textPart->getText(), $restoredText->getText()); // Test with file $file = new File('https://example.com/doc.pdf', 'application/pdf'); $filePart = new MessagePart($file); - $fileJson = $filePart->jsonSerialize(); - $restoredFile = MessagePart::fromJson($fileJson); + $fileJson = $filePart->toArray(); + $restoredFile = MessagePart::fromArray($fileJson); $this->assertEquals($file->getUrl(), $restoredFile->getFile()->getUrl()); $this->assertEquals($file->getMimeType(), $restoredFile->getFile()->getMimeType()); // Test with function call $functionCall = new FunctionCall('id_123', 'getData', ['key' => 'value']); $funcPart = new MessagePart($functionCall); - $funcJson = $funcPart->jsonSerialize(); - $restoredFunc = MessagePart::fromJson($funcJson); + $funcJson = $funcPart->toArray(); + $restoredFunc = MessagePart::fromArray($funcJson); $this->assertEquals($functionCall->getId(), $restoredFunc->getFunctionCall()->getId()); $this->assertEquals($functionCall->getName(), $restoredFunc->getFunctionCall()->getName()); $this->assertEquals($functionCall->getArgs(), $restoredFunc->getFunctionCall()->getArgs()); } /** - * Tests MessagePart implements WithJsonSerialization. + * Tests MessagePart implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $part = new MessagePart('test'); $this->assertInstanceOf( - \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, - $part - ); - $this->assertInstanceOf( - \JsonSerializable::class, + \WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface::class, $part ); + } } \ No newline at end of file diff --git a/tests/unit/Messages/DTO/MessageTest.php b/tests/unit/Messages/DTO/MessageTest.php index a7b178c7..84781f26 100644 --- a/tests/unit/Messages/DTO/MessageTest.php +++ b/tests/unit/Messages/DTO/MessageTest.php @@ -230,11 +230,11 @@ public function testModelMessageWithFunctionResponse(): void } /** - * Tests JSON serialization. + * Tests array transformation. * * @return void */ - public function testJsonSerialize(): void + public function testToArray(): void { $role = MessageRoleEnum::user(); $parts = [ @@ -242,7 +242,7 @@ public function testJsonSerialize(): void new MessagePart('How are you?') ]; $message = new Message($role, $parts); - $json = $message->jsonSerialize(); + $json = $message->toArray(); $this->assertIsArray($json); $this->assertEquals($role->value, $json['role']); @@ -257,7 +257,7 @@ public function testJsonSerialize(): void * * @return void */ - public function testFromJson(): void + public function testFromArray(): void { $json = [ 'role' => MessageRoleEnum::system()->value, @@ -266,7 +266,7 @@ public function testFromJson(): void ] ]; - $message = Message::fromJson($json); + $message = Message::fromArray($json); $this->assertInstanceOf(Message::class, $message); $this->assertEquals(MessageRoleEnum::system(), $message->getRole()); @@ -275,11 +275,11 @@ public function testFromJson(): void } /** - * Tests round-trip JSON serialization. + * Tests round-trip array transformation. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { $original = new Message( MessageRoleEnum::model(), @@ -289,8 +289,8 @@ public function testJsonRoundTrip(): void ] ); - $json = $original->jsonSerialize(); - $restored = Message::fromJson($json); + $json = $original->toArray(); + $restored = Message::fromArray($json); $this->assertEquals($original->getRole()->value, $restored->getRole()->value); $this->assertCount(count($original->getParts()), $restored->getParts()); @@ -302,21 +302,18 @@ public function testJsonRoundTrip(): void } /** - * Tests Message implements WithJsonSerialization. + * Tests Message implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $message = new Message(MessageRoleEnum::user(), [new MessagePart('test')]); $this->assertInstanceOf( - \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, - $message - ); - $this->assertInstanceOf( - \JsonSerializable::class, + \WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface::class, $message ); + } } \ No newline at end of file diff --git a/tests/unit/Messages/DTO/ModelMessageTest.php b/tests/unit/Messages/DTO/ModelMessageTest.php index 3fafb01d..7f0352c6 100644 --- a/tests/unit/Messages/DTO/ModelMessageTest.php +++ b/tests/unit/Messages/DTO/ModelMessageTest.php @@ -10,7 +10,7 @@ use WordPress\AiClient\Messages\DTO\ModelMessage; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; -use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait; use WordPress\AiClient\Tools\DTO\FunctionCall; use WordPress\AiClient\Tools\DTO\FunctionResponse; @@ -19,7 +19,7 @@ */ class ModelMessageTest extends TestCase { - use JsonSerializationTestTrait; + use ArrayTransformationTestTrait; /** * Tests creating ModelMessage automatically sets MODEL role. @@ -123,20 +123,20 @@ public function testJsonSchemaInheritance(): void } /** - * Tests JSON serialization. + * Tests array transformation. * * @return void */ - public function testJsonSerialize(): void + public function testToArray(): void { $message = new ModelMessage([ new MessagePart('I can help you with that.'), new MessagePart('Here is the solution:') ]); - $json = $this->assertJsonSerializeReturnsArray($message); + $json = $this->assertToArrayReturnsArray($message); - $this->assertJsonHasKeys($json, ['role', 'parts']); + $this->assertArrayHasKeys($json, ['role', 'parts']); $this->assertEquals(MessageRoleEnum::model()->value, $json['role']); $this->assertCount(2, $json['parts']); $this->assertEquals('I can help you with that.', $json['parts'][0]['text']); @@ -148,7 +148,7 @@ public function testJsonSerialize(): void * * @return void */ - public function testFromJson(): void + public function testFromArray(): void { $json = [ 'role' => MessageRoleEnum::model()->value, @@ -158,7 +158,7 @@ public function testFromJson(): void ] ]; - $message = ModelMessage::fromJson($json); + $message = ModelMessage::fromArray($json); $this->assertInstanceOf(ModelMessage::class, $message); $this->assertEquals(MessageRoleEnum::model(), $message->getRole()); @@ -168,13 +168,13 @@ public function testFromJson(): void } /** - * Tests round-trip JSON serialization with function call. + * Tests round-trip array transformation with function call. * * @return void */ - public function testJsonRoundTripWithFunctionCall(): void + public function testArrayRoundTripWithFunctionCall(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new ModelMessage([ new MessagePart('I\'ll search for that information.'), new MessagePart(new FunctionCall('search_123', 'webSearch', ['query' => 'PHP 8 features'])) @@ -199,13 +199,13 @@ function ($original, $restored) { } /** - * Tests ModelMessage implements WithJsonSerialization. + * Tests ModelMessage implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $message = new ModelMessage([new MessagePart('test')]); - $this->assertImplementsJsonSerialization($message); + $this->assertImplementsArrayTransformation($message); } } \ No newline at end of file diff --git a/tests/unit/Messages/DTO/SystemMessageTest.php b/tests/unit/Messages/DTO/SystemMessageTest.php index 5e3ce8c3..e716091c 100644 --- a/tests/unit/Messages/DTO/SystemMessageTest.php +++ b/tests/unit/Messages/DTO/SystemMessageTest.php @@ -9,14 +9,14 @@ use WordPress\AiClient\Messages\DTO\SystemMessage; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; -use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait; /** * @covers \WordPress\AiClient\Messages\DTO\SystemMessage */ class SystemMessageTest extends TestCase { - use JsonSerializationTestTrait; + use ArrayTransformationTestTrait; /** * Tests creating SystemMessage automatically sets SYSTEM role. @@ -169,20 +169,20 @@ public function testPreservesPartOrder(): void } /** - * Tests JSON serialization. + * Tests array transformation. * * @return void */ - public function testJsonSerialize(): void + public function testToArray(): void { $message = new SystemMessage([ new MessagePart('You are a helpful assistant.'), new MessagePart('Always be respectful and accurate.') ]); - $json = $this->assertJsonSerializeReturnsArray($message); + $json = $this->assertToArrayReturnsArray($message); - $this->assertJsonHasKeys($json, ['role', 'parts']); + $this->assertArrayHasKeys($json, ['role', 'parts']); $this->assertEquals(MessageRoleEnum::system()->value, $json['role']); $this->assertCount(2, $json['parts']); $this->assertEquals('You are a helpful assistant.', $json['parts'][0]['text']); @@ -194,7 +194,7 @@ public function testJsonSerialize(): void * * @return void */ - public function testFromJson(): void + public function testFromArray(): void { $json = [ 'role' => MessageRoleEnum::system()->value, @@ -204,7 +204,7 @@ public function testFromJson(): void ] ]; - $message = SystemMessage::fromJson($json); + $message = SystemMessage::fromArray($json); $this->assertInstanceOf(SystemMessage::class, $message); $this->assertEquals(MessageRoleEnum::system(), $message->getRole()); @@ -214,13 +214,13 @@ public function testFromJson(): void } /** - * Tests round-trip JSON serialization. + * Tests round-trip array transformation. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new SystemMessage([ new MessagePart('You are an expert in PHP.'), new MessagePart('Follow best practices.') @@ -241,13 +241,13 @@ function ($original, $restored) { } /** - * Tests SystemMessage implements WithJsonSerialization. + * Tests SystemMessage implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $message = new SystemMessage([new MessagePart('test')]); - $this->assertImplementsJsonSerialization($message); + $this->assertImplementsArrayTransformation($message); } } \ No newline at end of file diff --git a/tests/unit/Messages/DTO/UserMessageTest.php b/tests/unit/Messages/DTO/UserMessageTest.php index ab56e94e..392a7249 100644 --- a/tests/unit/Messages/DTO/UserMessageTest.php +++ b/tests/unit/Messages/DTO/UserMessageTest.php @@ -10,14 +10,14 @@ use WordPress\AiClient\Messages\DTO\UserMessage; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; -use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait; /** * @covers \WordPress\AiClient\Messages\DTO\UserMessage */ class UserMessageTest extends TestCase { - use JsonSerializationTestTrait; + use ArrayTransformationTestTrait; /** * Tests creating UserMessage automatically sets USER role. @@ -229,20 +229,20 @@ public function testWithMultipleFiles(): void } /** - * Tests JSON serialization. + * Tests array transformation. * * @return void */ - public function testJsonSerialize(): void + public function testToArray(): void { $message = new UserMessage([ new MessagePart('Hello, I need help'), new MessagePart('Can you assist?') ]); - $json = $this->assertJsonSerializeReturnsArray($message); + $json = $this->assertToArrayReturnsArray($message); - $this->assertJsonHasKeys($json, ['role', 'parts']); + $this->assertArrayHasKeys($json, ['role', 'parts']); $this->assertEquals(MessageRoleEnum::user()->value, $json['role']); $this->assertCount(2, $json['parts']); $this->assertEquals('Hello, I need help', $json['parts'][0]['text']); @@ -254,7 +254,7 @@ public function testJsonSerialize(): void * * @return void */ - public function testFromJson(): void + public function testFromArray(): void { $json = [ 'role' => MessageRoleEnum::user()->value, @@ -264,7 +264,7 @@ public function testFromJson(): void ] ]; - $message = UserMessage::fromJson($json); + $message = UserMessage::fromArray($json); $this->assertInstanceOf(UserMessage::class, $message); $this->assertEquals(MessageRoleEnum::user(), $message->getRole()); @@ -274,13 +274,13 @@ public function testFromJson(): void } /** - * Tests round-trip JSON serialization. + * Tests round-trip array transformation. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new UserMessage([ new MessagePart('Test message'), new MessagePart(new File('https://example.com/image.jpg', 'image/jpeg')) @@ -301,13 +301,13 @@ function ($original, $restored) { } /** - * Tests UserMessage implements WithJsonSerialization. + * Tests UserMessage implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $message = new UserMessage([new MessagePart('test')]); - $this->assertImplementsJsonSerialization($message); + $this->assertImplementsArrayTransformation($message); } } \ No newline at end of file diff --git a/tests/unit/Operations/DTO/GenerativeAiOperationTest.php b/tests/unit/Operations/DTO/GenerativeAiOperationTest.php index 9287baad..40e4cc9a 100644 --- a/tests/unit/Operations/DTO/GenerativeAiOperationTest.php +++ b/tests/unit/Operations/DTO/GenerativeAiOperationTest.php @@ -15,14 +15,14 @@ use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Results\DTO\TokenUsage; use WordPress\AiClient\Results\Enums\FinishReasonEnum; -use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait; /** * @covers \WordPress\AiClient\Operations\DTO\GenerativeAiOperation */ class GenerativeAiOperationTest extends TestCase { - use JsonSerializationTestTrait; + use ArrayTransformationTestTrait; /** * Tests creating operation in starting state. * @@ -292,31 +292,31 @@ public function testWithEmptyStringId(): void } /** - * Tests JSON serialization for operation in starting state. + * Tests array transformation for operation in starting state. * * @return void */ - public function testJsonSerializeStartingState(): void + public function testToArrayStartingState(): void { $operation = new GenerativeAiOperation( 'op_start_123', OperationStateEnum::starting() ); - $json = $this->assertJsonSerializeReturnsArray($operation); + $json = $this->assertToArrayReturnsArray($operation); - $this->assertJsonHasKeys($json, ['id', 'state']); - $this->assertJsonNotHasKeys($json, ['result']); + $this->assertArrayHasKeys($json, ['id', 'state']); + $this->assertArrayNotHasKeys($json, ['result']); $this->assertEquals('op_start_123', $json['id']); $this->assertEquals(OperationStateEnum::starting()->value, $json['state']); } /** - * Tests JSON serialization for operation in succeeded state. + * Tests array transformation for operation in succeeded state. * * @return void */ - public function testJsonSerializeSucceededState(): void + public function testToArraySucceededState(): void { $modelMessage = new ModelMessage([ new MessagePart('Success response') @@ -339,9 +339,9 @@ public function testJsonSerializeSucceededState(): void $result ); - $json = $this->assertJsonSerializeReturnsArray($operation); + $json = $this->assertToArrayReturnsArray($operation); - $this->assertJsonHasKeys($json, ['id', 'state', 'result']); + $this->assertArrayHasKeys($json, ['id', 'state', 'result']); $this->assertEquals('op_success_456', $json['id']); $this->assertEquals(OperationStateEnum::succeeded()->value, $json['state']); $this->assertIsArray($json['result']); @@ -353,14 +353,14 @@ public function testJsonSerializeSucceededState(): void * * @return void */ - public function testFromJsonStartingState(): void + public function testFromArrayStartingState(): void { $json = [ 'id' => 'op_from_json_start', 'state' => OperationStateEnum::starting()->value ]; - $operation = GenerativeAiOperation::fromJson($json); + $operation = GenerativeAiOperation::fromArray($json); $this->assertInstanceOf(GenerativeAiOperation::class, $operation); $this->assertEquals('op_from_json_start', $operation->getId()); @@ -373,7 +373,7 @@ public function testFromJsonStartingState(): void * * @return void */ - public function testFromJsonSucceededState(): void + public function testFromArraySucceededState(): void { $json = [ 'id' => 'op_from_json_success', @@ -398,7 +398,7 @@ public function testFromJsonSucceededState(): void ] ]; - $operation = GenerativeAiOperation::fromJson($json); + $operation = GenerativeAiOperation::fromArray($json); $this->assertInstanceOf(GenerativeAiOperation::class, $operation); $this->assertEquals('op_from_json_success', $operation->getId()); @@ -408,13 +408,13 @@ public function testFromJsonSucceededState(): void } /** - * Tests round-trip JSON serialization for processing state. + * Tests round-trip array transformation for processing state. * * @return void */ - public function testJsonRoundTripProcessingState(): void + public function testArrayRoundTripProcessingState(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new GenerativeAiOperation( 'op_roundtrip_process', OperationStateEnum::processing() @@ -428,11 +428,11 @@ function ($original, $restored) { } /** - * Tests round-trip JSON serialization for succeeded state. + * Tests round-trip array transformation for succeeded state. * * @return void */ - public function testJsonRoundTripSucceededState(): void + public function testArrayRoundTripSucceededState(): void { $modelMessage = new ModelMessage([ new MessagePart('Roundtrip test response') @@ -449,7 +449,7 @@ public function testJsonRoundTripSucceededState(): void $tokenUsage ); - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new GenerativeAiOperation( 'op_roundtrip_success', OperationStateEnum::succeeded(), @@ -465,16 +465,16 @@ function ($original, $restored) { } /** - * Tests GenerativeAiOperation implements WithJsonSerialization. + * Tests GenerativeAiOperation implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $operation = new GenerativeAiOperation( 'op_test', OperationStateEnum::starting() ); - $this->assertImplementsJsonSerialization($operation); + $this->assertImplementsArrayTransformation($operation); } } \ No newline at end of file diff --git a/tests/unit/Results/DTO/CandidateTest.php b/tests/unit/Results/DTO/CandidateTest.php index b6d38d62..92b8e6b3 100644 --- a/tests/unit/Results/DTO/CandidateTest.php +++ b/tests/unit/Results/DTO/CandidateTest.php @@ -14,7 +14,7 @@ use WordPress\AiClient\Messages\Enums\MessageRoleEnum; use WordPress\AiClient\Results\DTO\Candidate; use WordPress\AiClient\Results\Enums\FinishReasonEnum; -use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait; use WordPress\AiClient\Tools\DTO\FunctionCall; /** @@ -22,7 +22,7 @@ */ class CandidateTest extends TestCase { - use JsonSerializationTestTrait; + use ArrayTransformationTestTrait; /** * Tests creating candidate with basic properties. * @@ -334,11 +334,11 @@ public function testWithErrorFinishReason(): void } /** - * Tests JSON serialization. + * Tests array transformation. * * @return void */ - public function testJsonSerialize(): void + public function testToArray(): void { $message = new ModelMessage([ new MessagePart('This is the AI response.'), @@ -351,9 +351,9 @@ public function testJsonSerialize(): void 45 ); - $json = $this->assertJsonSerializeReturnsArray($candidate); + $json = $this->assertToArrayReturnsArray($candidate); - $this->assertJsonHasKeys($json, ['message', 'finishReason', 'tokenCount']); + $this->assertArrayHasKeys($json, ['message', 'finishReason', 'tokenCount']); $this->assertIsArray($json['message']); $this->assertEquals(FinishReasonEnum::stop()->value, $json['finishReason']); $this->assertEquals(45, $json['tokenCount']); @@ -364,7 +364,7 @@ public function testJsonSerialize(): void * * @return void */ - public function testFromJson(): void + public function testFromArray(): void { $json = [ 'message' => [ @@ -378,7 +378,7 @@ public function testFromJson(): void 'tokenCount' => 75 ]; - $candidate = Candidate::fromJson($json); + $candidate = Candidate::fromArray($json); $this->assertInstanceOf(Candidate::class, $candidate); $this->assertEquals(FinishReasonEnum::stop(), $candidate->getFinishReason()); @@ -389,13 +389,13 @@ public function testFromJson(): void } /** - * Tests round-trip JSON serialization. + * Tests round-trip array transformation. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new Candidate( new ModelMessage([ new MessagePart('Generated response'), @@ -424,17 +424,17 @@ function ($original, $restored) { } /** - * Tests Candidate implements WithJsonSerialization. + * Tests Candidate implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $candidate = new Candidate( new ModelMessage([new MessagePart('test')]), FinishReasonEnum::stop(), 10 ); - $this->assertImplementsJsonSerialization($candidate); + $this->assertImplementsArrayTransformation($candidate); } } \ No newline at end of file diff --git a/tests/unit/Results/DTO/GenerativeAiResultTest.php b/tests/unit/Results/DTO/GenerativeAiResultTest.php index efd4d79f..9d65fe0b 100644 --- a/tests/unit/Results/DTO/GenerativeAiResultTest.php +++ b/tests/unit/Results/DTO/GenerativeAiResultTest.php @@ -14,7 +14,7 @@ use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Results\DTO\TokenUsage; use WordPress\AiClient\Results\Enums\FinishReasonEnum; -use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait; use WordPress\AiClient\Tools\DTO\FunctionCall; /** @@ -22,7 +22,7 @@ */ class GenerativeAiResultTest extends TestCase { - use JsonSerializationTestTrait; + use ArrayTransformationTestTrait; /** * Tests creating result with single candidate. * @@ -601,11 +601,11 @@ public function testHasMultipleCandidatesReturnsFalseForSingle(): void } /** - * Tests JSON serialization. + * Tests array transformation. * * @return void */ - public function testJsonSerialize(): void + public function testToArray(): void { $message = new ModelMessage([ new MessagePart('AI generated response'), @@ -622,9 +622,9 @@ public function testJsonSerialize(): void $metadata ); - $json = $this->assertJsonSerializeReturnsArray($result); + $json = $this->assertToArrayReturnsArray($result); - $this->assertJsonHasKeys($json, ['id', 'candidates', 'tokenUsage', 'providerMetadata']); + $this->assertArrayHasKeys($json, ['id', 'candidates', 'tokenUsage', 'providerMetadata']); $this->assertEquals('result_json_123', $json['id']); $this->assertIsArray($json['candidates']); $this->assertCount(1, $json['candidates']); @@ -637,7 +637,7 @@ public function testJsonSerialize(): void * * @return void */ - public function testFromJson(): void + public function testFromArray(): void { $json = [ 'id' => 'result_from_json', @@ -662,7 +662,7 @@ public function testFromJson(): void 'providerMetadata' => ['provider' => 'test'] ]; - $result = GenerativeAiResult::fromJson($json); + $result = GenerativeAiResult::fromArray($json); $this->assertInstanceOf(GenerativeAiResult::class, $result); $this->assertEquals('result_from_json', $result->getId()); @@ -674,11 +674,11 @@ public function testFromJson(): void } /** - * Tests round-trip JSON serialization with multiple candidates. + * Tests round-trip array transformation with multiple candidates. * * @return void */ - public function testJsonRoundTripWithMultipleCandidates(): void + public function testArrayRoundTripWithMultipleCandidates(): void { $candidates = []; for ($i = 1; $i <= 2; $i++) { @@ -689,7 +689,7 @@ public function testJsonRoundTripWithMultipleCandidates(): void $candidates[] = new Candidate($message, FinishReasonEnum::toolCalls(), 25 * $i); } - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new GenerativeAiResult( 'result_roundtrip', $candidates, @@ -719,11 +719,11 @@ function ($original, $restored) { } /** - * Tests JSON serialization without provider metadata. + * Tests array transformation without provider metadata. * * @return void */ - public function testJsonSerializeWithoutProviderMetadata(): void + public function testToArrayWithoutProviderMetadata(): void { $message = new ModelMessage([new MessagePart('Simple response')]); $candidate = new Candidate($message, FinishReasonEnum::stop(), 5); @@ -735,24 +735,24 @@ public function testJsonSerializeWithoutProviderMetadata(): void $tokenUsage ); - $json = $this->assertJsonSerializeReturnsArray($result); + $json = $this->assertToArrayReturnsArray($result); - $this->assertJsonHasKeys($json, ['id', 'candidates', 'tokenUsage', 'providerMetadata']); + $this->assertArrayHasKeys($json, ['id', 'candidates', 'tokenUsage', 'providerMetadata']); $this->assertEquals([], $json['providerMetadata']); } /** - * Tests GenerativeAiResult implements WithJsonSerialization. + * Tests GenerativeAiResult implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $message = new ModelMessage([new MessagePart('test')]); $candidate = new Candidate($message, FinishReasonEnum::stop(), 1); $tokenUsage = new TokenUsage(1, 1, 2); $result = new GenerativeAiResult('test', [$candidate], $tokenUsage); - $this->assertImplementsJsonSerialization($result); + $this->assertImplementsArrayTransformation($result); } } \ No newline at end of file diff --git a/tests/unit/Results/DTO/TokenUsageTest.php b/tests/unit/Results/DTO/TokenUsageTest.php index a4f12ea5..aed0cb60 100644 --- a/tests/unit/Results/DTO/TokenUsageTest.php +++ b/tests/unit/Results/DTO/TokenUsageTest.php @@ -209,14 +209,14 @@ public function testMultipleInstances(): void } /** - * Tests JSON serialization. + * Tests array transformation. * * @return void */ - public function testJsonSerialize(): void + public function testToArray(): void { $tokenUsage = new TokenUsage(100, 50, 150); - $json = $tokenUsage->jsonSerialize(); + $json = $tokenUsage->toArray(); $this->assertIsArray($json); $this->assertArrayHasKey('promptTokens', $json); @@ -233,7 +233,7 @@ public function testJsonSerialize(): void * * @return void */ - public function testFromJson(): void + public function testFromArray(): void { $json = [ 'promptTokens' => 100, @@ -241,7 +241,7 @@ public function testFromJson(): void 'totalTokens' => 150, ]; - $tokenUsage = TokenUsage::fromJson($json); + $tokenUsage = TokenUsage::fromArray($json); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); $this->assertEquals(100, $tokenUsage->getPromptTokens()); @@ -250,15 +250,15 @@ public function testFromJson(): void } /** - * Tests round-trip JSON serialization. + * Tests round-trip array transformation. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { $original = new TokenUsage(123, 456, 579); - $json = $original->jsonSerialize(); - $restored = TokenUsage::fromJson($json); + $json = $original->toArray(); + $restored = TokenUsage::fromArray($json); $this->assertEquals($original->getPromptTokens(), $restored->getPromptTokens()); $this->assertEquals($original->getCompletionTokens(), $restored->getCompletionTokens()); @@ -266,22 +266,19 @@ public function testJsonRoundTrip(): void } /** - * Tests TokenUsage implements WithJsonSerialization. + * Tests TokenUsage implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $tokenUsage = new TokenUsage(10, 20, 30); $this->assertInstanceOf( - \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, - $tokenUsage - ); - $this->assertInstanceOf( - \JsonSerializable::class, + \WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface::class, $tokenUsage ); + } /** diff --git a/tests/unit/Tools/DTO/FunctionCallTest.php b/tests/unit/Tools/DTO/FunctionCallTest.php index 6342a4ca..25b7c175 100644 --- a/tests/unit/Tools/DTO/FunctionCallTest.php +++ b/tests/unit/Tools/DTO/FunctionCallTest.php @@ -163,14 +163,14 @@ public function testWithComplexArgs(): void } /** - * Tests JSON serialization with all fields. + * Tests array transformation with all fields. * * @return void */ - public function testJsonSerializeAllFields(): void + public function testToArrayAllFields(): void { $functionCall = new FunctionCall('func_123', 'calculate', ['x' => 10, 'y' => 20]); - $json = $functionCall->jsonSerialize(); + $json = $functionCall->toArray(); $this->assertIsArray($json); $this->assertEquals('func_123', $json['id']); @@ -179,14 +179,14 @@ public function testJsonSerializeAllFields(): void } /** - * Tests JSON serialization with only ID. + * Tests array transformation with only ID. * * @return void */ - public function testJsonSerializeOnlyId(): void + public function testToArrayOnlyId(): void { $functionCall = new FunctionCall('func_456', null); - $json = $functionCall->jsonSerialize(); + $json = $functionCall->toArray(); $this->assertIsArray($json); $this->assertEquals('func_456', $json['id']); @@ -195,14 +195,14 @@ public function testJsonSerializeOnlyId(): void } /** - * Tests JSON serialization with only name. + * Tests array transformation with only name. * * @return void */ - public function testJsonSerializeOnlyName(): void + public function testToArrayOnlyName(): void { $functionCall = new FunctionCall(null, 'search'); - $json = $functionCall->jsonSerialize(); + $json = $functionCall->toArray(); $this->assertIsArray($json); $this->assertEquals('search', $json['name']); @@ -215,7 +215,7 @@ public function testJsonSerializeOnlyName(): void * * @return void */ - public function testFromJsonAllFields(): void + public function testFromArrayAllFields(): void { $json = [ 'id' => 'func_789', @@ -223,7 +223,7 @@ public function testFromJsonAllFields(): void 'args' => ['input' => 'data', 'format' => 'json'] ]; - $functionCall = FunctionCall::fromJson($json); + $functionCall = FunctionCall::fromArray($json); $this->assertInstanceOf(FunctionCall::class, $functionCall); $this->assertEquals('func_789', $functionCall->getId()); @@ -236,11 +236,11 @@ public function testFromJsonAllFields(): void * * @return void */ - public function testFromJsonMinimalFields(): void + public function testFromArrayMinimalFields(): void { $json = ['name' => 'minimal']; - $functionCall = FunctionCall::fromJson($json); + $functionCall = FunctionCall::fromArray($json); $this->assertInstanceOf(FunctionCall::class, $functionCall); $this->assertNull($functionCall->getId()); @@ -249,15 +249,15 @@ public function testFromJsonMinimalFields(): void } /** - * Tests round-trip JSON serialization. + * Tests round-trip array transformation. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { $original = new FunctionCall('id_123', 'execute', ['param' => 'value', 'count' => 5]); - $json = $original->jsonSerialize(); - $restored = FunctionCall::fromJson($json); + $json = $original->toArray(); + $restored = FunctionCall::fromArray($json); $this->assertEquals($original->getId(), $restored->getId()); $this->assertEquals($original->getName(), $restored->getName()); @@ -265,21 +265,18 @@ public function testJsonRoundTrip(): void } /** - * Tests FunctionCall implements WithJsonSerialization. + * Tests FunctionCall implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $functionCall = new FunctionCall('id', 'name'); $this->assertInstanceOf( - \WordPress\AiClient\Common\Contracts\WithJsonSerialization::class, - $functionCall - ); - $this->assertInstanceOf( - \JsonSerializable::class, + \WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface::class, $functionCall ); + } } \ No newline at end of file diff --git a/tests/unit/Tools/DTO/FunctionDeclarationTest.php b/tests/unit/Tools/DTO/FunctionDeclarationTest.php index 3d00241a..c18ebb97 100644 --- a/tests/unit/Tools/DTO/FunctionDeclarationTest.php +++ b/tests/unit/Tools/DTO/FunctionDeclarationTest.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Tests\unit\Tools\DTO; use PHPUnit\Framework\TestCase; -use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait; use WordPress\AiClient\Tools\DTO\FunctionDeclaration; /** @@ -13,7 +13,7 @@ */ class FunctionDeclarationTest extends TestCase { - use JsonSerializationTestTrait; + use ArrayTransformationTestTrait; /** * Tests creating FunctionDeclaration with all properties. * @@ -189,11 +189,11 @@ public function testWithOpenApiStyleSchema(): void } /** - * Tests JSON serialization with parameters. + * Tests array transformation with parameters. * * @return void */ - public function testJsonSerializeWithParameters(): void + public function testToArrayWithParameters(): void { $declaration = new FunctionDeclaration( 'searchWeb', @@ -201,29 +201,29 @@ public function testJsonSerializeWithParameters(): void ['type' => 'object', 'properties' => ['query' => ['type' => 'string']]] ); - $json = $this->assertJsonSerializeReturnsArray($declaration); + $json = $this->assertToArrayReturnsArray($declaration); - $this->assertJsonHasKeys($json, ['name', 'description', 'parameters']); + $this->assertArrayHasKeys($json, ['name', 'description', 'parameters']); $this->assertEquals('searchWeb', $json['name']); $this->assertEquals('Searches the web for information', $json['description']); $this->assertEquals(['type' => 'object', 'properties' => ['query' => ['type' => 'string']]], $json['parameters']); } /** - * Tests JSON serialization without parameters. + * Tests array transformation without parameters. * * @return void */ - public function testJsonSerializeWithoutParameters(): void + public function testToArrayWithoutParameters(): void { $declaration = new FunctionDeclaration( 'getTimestamp', 'Returns the current Unix timestamp' ); - $json = $this->assertJsonSerializeReturnsArray($declaration); + $json = $this->assertToArrayReturnsArray($declaration); - $this->assertJsonHasKeys($json, ['name', 'description']); + $this->assertArrayHasKeys($json, ['name', 'description']); $this->assertArrayNotHasKey('parameters', $json); $this->assertEquals('getTimestamp', $json['name']); $this->assertEquals('Returns the current Unix timestamp', $json['description']); @@ -234,7 +234,7 @@ public function testJsonSerializeWithoutParameters(): void * * @return void */ - public function testFromJsonWithParameters(): void + public function testFromArrayWithParameters(): void { $json = [ 'name' => 'calculateArea', @@ -249,7 +249,7 @@ public function testFromJsonWithParameters(): void ] ]; - $declaration = FunctionDeclaration::fromJson($json); + $declaration = FunctionDeclaration::fromArray($json); $this->assertInstanceOf(FunctionDeclaration::class, $declaration); $this->assertEquals('calculateArea', $declaration->getName()); @@ -262,14 +262,14 @@ public function testFromJsonWithParameters(): void * * @return void */ - public function testFromJsonWithoutParameters(): void + public function testFromArrayWithoutParameters(): void { $json = [ 'name' => 'ping', 'description' => 'Simple ping function' ]; - $declaration = FunctionDeclaration::fromJson($json); + $declaration = FunctionDeclaration::fromArray($json); $this->assertInstanceOf(FunctionDeclaration::class, $declaration); $this->assertEquals('ping', $declaration->getName()); @@ -278,13 +278,13 @@ public function testFromJsonWithoutParameters(): void } /** - * Tests round-trip JSON serialization. + * Tests round-trip array transformation. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new FunctionDeclaration( 'complexFunction', 'A complex function with nested parameters', @@ -314,13 +314,13 @@ function ($original, $restored) { } /** - * Tests FunctionDeclaration implements WithJsonSerialization. + * Tests FunctionDeclaration implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $declaration = new FunctionDeclaration('test', 'test function'); - $this->assertImplementsJsonSerialization($declaration); + $this->assertImplementsArrayTransformation($declaration); } } \ No newline at end of file diff --git a/tests/unit/Tools/DTO/FunctionResponseTest.php b/tests/unit/Tools/DTO/FunctionResponseTest.php index 1f707c26..5606ef89 100644 --- a/tests/unit/Tools/DTO/FunctionResponseTest.php +++ b/tests/unit/Tools/DTO/FunctionResponseTest.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Tests\unit\Tools\DTO; use PHPUnit\Framework\TestCase; -use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait; use WordPress\AiClient\Tools\DTO\FunctionResponse; /** @@ -13,7 +13,7 @@ */ class FunctionResponseTest extends TestCase { - use JsonSerializationTestTrait; + use ArrayTransformationTestTrait; /** * Tests creating FunctionResponse with all properties. @@ -185,16 +185,16 @@ public function testWithLargeResponseData(): void } /** - * Tests JSON serialization. + * Tests array transformation. * * @return void */ - public function testJsonSerialize(): void + public function testToArray(): void { $response = new FunctionResponse('func_123', 'calculate', ['result' => 42]); - $json = $this->assertJsonSerializeReturnsArray($response); + $json = $this->assertToArrayReturnsArray($response); - $this->assertJsonHasKeys($json, ['id', 'name', 'response']); + $this->assertArrayHasKeys($json, ['id', 'name', 'response']); $this->assertEquals('func_123', $json['id']); $this->assertEquals('calculate', $json['name']); $this->assertEquals(['result' => 42], $json['response']); @@ -205,7 +205,7 @@ public function testJsonSerialize(): void * * @return void */ - public function testFromJson(): void + public function testFromArray(): void { $json = [ 'id' => 'func_456', @@ -213,7 +213,7 @@ public function testFromJson(): void 'response' => ['found' => true, 'count' => 5] ]; - $response = FunctionResponse::fromJson($json); + $response = FunctionResponse::fromArray($json); $this->assertInstanceOf(FunctionResponse::class, $response); $this->assertEquals('func_456', $response->getId()); @@ -222,13 +222,13 @@ public function testFromJson(): void } /** - * Tests round-trip JSON serialization. + * Tests round-trip array transformation. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new FunctionResponse('id_789', 'process', ['status' => 'complete']), function ($original, $restored) { $this->assertEquals($original->getId(), $restored->getId()); @@ -239,13 +239,13 @@ function ($original, $restored) { } /** - * Tests FunctionResponse implements WithJsonSerialization. + * Tests FunctionResponse implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $response = new FunctionResponse('id', 'name', 'result'); - $this->assertImplementsJsonSerialization($response); + $this->assertImplementsArrayTransformation($response); } } \ No newline at end of file diff --git a/tests/unit/Tools/DTO/ToolTest.php b/tests/unit/Tools/DTO/ToolTest.php index 6c74c905..e951023b 100644 --- a/tests/unit/Tools/DTO/ToolTest.php +++ b/tests/unit/Tools/DTO/ToolTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Providers\Enums\ToolTypeEnum; -use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait; use WordPress\AiClient\Tools\DTO\FunctionDeclaration; use WordPress\AiClient\Tools\DTO\Tool; use WordPress\AiClient\Tools\DTO\WebSearch; @@ -16,7 +16,7 @@ */ class ToolTest extends TestCase { - use JsonSerializationTestTrait; + use ArrayTransformationTestTrait; /** * Tests creating tool with function declarations. * @@ -299,11 +299,11 @@ public function testMultipleToolInstances(): void } /** - * Tests JSON serialization with function declarations. + * Tests array transformation with function declarations. * * @return void */ - public function testJsonSerializeWithFunctionDeclarations(): void + public function testToArrayWithFunctionDeclarations(): void { $functions = [ new FunctionDeclaration('func1', 'First function', ['param1' => ['type' => 'string']]), @@ -311,9 +311,9 @@ public function testJsonSerializeWithFunctionDeclarations(): void ]; $tool = new Tool($functions); - $json = $this->assertJsonSerializeReturnsArray($tool); + $json = $this->assertToArrayReturnsArray($tool); - $this->assertJsonHasKeys($json, ['type', 'functionDeclarations']); + $this->assertArrayHasKeys($json, ['type', 'functionDeclarations']); $this->assertEquals(ToolTypeEnum::functionDeclarations()->value, $json['type']); $this->assertIsArray($json['functionDeclarations']); $this->assertCount(2, $json['functionDeclarations']); @@ -322,11 +322,11 @@ public function testJsonSerializeWithFunctionDeclarations(): void } /** - * Tests JSON serialization with web search. + * Tests array transformation with web search. * * @return void */ - public function testJsonSerializeWithWebSearch(): void + public function testToArrayWithWebSearch(): void { $webSearch = new WebSearch( ['allowed1.com', 'allowed2.com'], @@ -334,9 +334,9 @@ public function testJsonSerializeWithWebSearch(): void ); $tool = new Tool($webSearch); - $json = $this->assertJsonSerializeReturnsArray($tool); + $json = $this->assertToArrayReturnsArray($tool); - $this->assertJsonHasKeys($json, ['type', 'webSearch']); + $this->assertArrayHasKeys($json, ['type', 'webSearch']); $this->assertEquals(ToolTypeEnum::webSearch()->value, $json['type']); $this->assertIsArray($json['webSearch']); $this->assertArrayHasKey('allowedDomains', $json['webSearch']); @@ -348,7 +348,7 @@ public function testJsonSerializeWithWebSearch(): void * * @return void */ - public function testFromJsonWithFunctionDeclarations(): void + public function testFromArrayWithFunctionDeclarations(): void { $json = [ 'type' => ToolTypeEnum::functionDeclarations()->value, @@ -361,7 +361,7 @@ public function testFromJsonWithFunctionDeclarations(): void ] ]; - $tool = Tool::fromJson($json); + $tool = Tool::fromArray($json); $this->assertInstanceOf(Tool::class, $tool); $this->assertEquals(ToolTypeEnum::functionDeclarations(), $tool->getType()); @@ -375,7 +375,7 @@ public function testFromJsonWithFunctionDeclarations(): void * * @return void */ - public function testFromJsonWithWebSearch(): void + public function testFromArrayWithWebSearch(): void { $json = [ 'type' => ToolTypeEnum::webSearch()->value, @@ -385,7 +385,7 @@ public function testFromJsonWithWebSearch(): void ] ]; - $tool = Tool::fromJson($json); + $tool = Tool::fromArray($json); $this->assertInstanceOf(Tool::class, $tool); $this->assertEquals(ToolTypeEnum::webSearch(), $tool->getType()); @@ -396,13 +396,13 @@ public function testFromJsonWithWebSearch(): void } /** - * Tests round-trip JSON serialization with function declarations. + * Tests round-trip array transformation with function declarations. * * @return void */ - public function testJsonRoundTripWithFunctionDeclarations(): void + public function testArrayRoundTripWithFunctionDeclarations(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new Tool([ new FunctionDeclaration('calculate', 'Performs calculations', ['expr' => ['type' => 'string']]), new FunctionDeclaration('validate', 'Validates input', ['data' => ['type' => 'object']]) @@ -424,13 +424,13 @@ function ($original, $restored) { } /** - * Tests round-trip JSON serialization with web search. + * Tests round-trip array transformation with web search. * * @return void */ - public function testJsonRoundTripWithWebSearch(): void + public function testArrayRoundTripWithWebSearch(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new Tool(new WebSearch(['docs.example.com'], ['ads.example.com'])), function ($original, $restored) { $this->assertEquals($original->getType()->value, $restored->getType()->value); @@ -447,13 +447,13 @@ function ($original, $restored) { } /** - * Tests Tool implements WithJsonSerialization. + * Tests Tool implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $tool = new Tool([]); - $this->assertImplementsJsonSerialization($tool); + $this->assertImplementsArrayTransformation($tool); } } \ No newline at end of file diff --git a/tests/unit/Tools/DTO/WebSearchTest.php b/tests/unit/Tools/DTO/WebSearchTest.php index 141f0da0..cb060251 100644 --- a/tests/unit/Tools/DTO/WebSearchTest.php +++ b/tests/unit/Tools/DTO/WebSearchTest.php @@ -5,7 +5,7 @@ namespace WordPress\AiClient\Tests\unit\Tools\DTO; use PHPUnit\Framework\TestCase; -use WordPress\AiClient\Tests\traits\JsonSerializationTestTrait; +use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait; use WordPress\AiClient\Tools\DTO\WebSearch; /** @@ -13,7 +13,7 @@ */ class WebSearchTest extends TestCase { - use JsonSerializationTestTrait; + use ArrayTransformationTestTrait; /** * Tests creating WebSearch with both allowed and disallowed domains. * @@ -295,52 +295,52 @@ public function testWithCommonDomainPatterns(): void } /** - * Tests JSON serialization with both domain lists. + * Tests array transformation with both domain lists. * * @return void */ - public function testJsonSerializeWithBothDomainLists(): void + public function testToArrayWithBothDomainLists(): void { $webSearch = new WebSearch( ['example.com', 'docs.example.com'], ['spam.com', 'malware.com'] ); - $json = $this->assertJsonSerializeReturnsArray($webSearch); + $json = $this->assertToArrayReturnsArray($webSearch); - $this->assertJsonHasKeys($json, ['allowedDomains', 'disallowedDomains']); + $this->assertArrayHasKeys($json, ['allowedDomains', 'disallowedDomains']); $this->assertEquals(['example.com', 'docs.example.com'], $json['allowedDomains']); $this->assertEquals(['spam.com', 'malware.com'], $json['disallowedDomains']); } /** - * Tests JSON serialization with empty domain lists. + * Tests array transformation with empty domain lists. * * @return void */ - public function testJsonSerializeWithEmptyDomainLists(): void + public function testToArrayWithEmptyDomainLists(): void { $webSearch = new WebSearch(); - $json = $this->assertJsonSerializeReturnsArray($webSearch); + $json = $this->assertToArrayReturnsArray($webSearch); - $this->assertJsonHasKeys($json, ['allowedDomains', 'disallowedDomains']); + $this->assertArrayHasKeys($json, ['allowedDomains', 'disallowedDomains']); $this->assertEquals([], $json['allowedDomains']); $this->assertEquals([], $json['disallowedDomains']); } /** - * Tests JSON serialization with only allowed domains. + * Tests array transformation with only allowed domains. * * @return void */ - public function testJsonSerializeWithOnlyAllowedDomains(): void + public function testToArrayWithOnlyAllowedDomains(): void { $webSearch = new WebSearch(['trusted1.com', 'trusted2.com']); - $json = $this->assertJsonSerializeReturnsArray($webSearch); + $json = $this->assertToArrayReturnsArray($webSearch); - $this->assertJsonHasKeys($json, ['allowedDomains', 'disallowedDomains']); + $this->assertArrayHasKeys($json, ['allowedDomains', 'disallowedDomains']); $this->assertEquals(['trusted1.com', 'trusted2.com'], $json['allowedDomains']); $this->assertEquals([], $json['disallowedDomains']); } @@ -350,14 +350,14 @@ public function testJsonSerializeWithOnlyAllowedDomains(): void * * @return void */ - public function testFromJsonWithBothDomainLists(): void + public function testFromArrayWithBothDomainLists(): void { $json = [ 'allowedDomains' => ['api.example.com', 'docs.example.com'], 'disallowedDomains' => ['ads.example.com', 'tracking.example.com'] ]; - $webSearch = WebSearch::fromJson($json); + $webSearch = WebSearch::fromArray($json); $this->assertInstanceOf(WebSearch::class, $webSearch); $this->assertEquals(['api.example.com', 'docs.example.com'], $webSearch->getAllowedDomains()); @@ -369,14 +369,14 @@ public function testFromJsonWithBothDomainLists(): void * * @return void */ - public function testFromJsonWithEmptyArrays(): void + public function testFromArrayWithEmptyArrays(): void { $json = [ 'allowedDomains' => [], 'disallowedDomains' => [] ]; - $webSearch = WebSearch::fromJson($json); + $webSearch = WebSearch::fromArray($json); $this->assertInstanceOf(WebSearch::class, $webSearch); $this->assertEquals([], $webSearch->getAllowedDomains()); @@ -388,11 +388,11 @@ public function testFromJsonWithEmptyArrays(): void * * @return void */ - public function testFromJsonWithMissingFieldsUsesDefaults(): void + public function testFromArrayWithMissingFieldsUsesDefaults(): void { $json = []; - $webSearch = WebSearch::fromJson($json); + $webSearch = WebSearch::fromArray($json); $this->assertInstanceOf(WebSearch::class, $webSearch); $this->assertEquals([], $webSearch->getAllowedDomains()); @@ -400,13 +400,13 @@ public function testFromJsonWithMissingFieldsUsesDefaults(): void } /** - * Tests round-trip JSON serialization. + * Tests round-trip array transformation. * * @return void */ - public function testJsonRoundTrip(): void + public function testArrayRoundTrip(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new WebSearch( ['wikipedia.org', 'arxiv.org', 'pubmed.gov'], ['facebook.com', 'twitter.com', 'instagram.com'] @@ -423,9 +423,9 @@ function ($original, $restored) { * * @return void */ - public function testJsonRoundTripWithSpecialCharacters(): void + public function testArrayRoundTripWithSpecialCharacters(): void { - $this->assertJsonRoundTrip( + $this->assertArrayRoundTrip( new WebSearch( ['example-with-dash.com', 'sub.domain.example.com', '192.168.1.1'], ['bad_underscore.com', 'another-dash.org'] @@ -438,13 +438,13 @@ function ($original, $restored) { } /** - * Tests WebSearch implements WithJsonSerialization. + * Tests WebSearch implements WithArrayTransformationInterface. * * @return void */ - public function testImplementsWithJsonSerialization(): void + public function testImplementsWithArrayTransformationInterface(): void { $webSearch = new WebSearch(); - $this->assertImplementsJsonSerialization($webSearch); + $this->assertImplementsArrayTransformation($webSearch); } } \ No newline at end of file From cb4406eaeed37d9322ae4bf75aba5886b2606ff4 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 15:33:57 -0600 Subject: [PATCH 10/36] refactor: adds abstract DTO with json serialization --- src/Common/AbstractDataValueObject.php | 100 ++++ src/Files/DTO/File.php | 7 +- src/Messages/DTO/Message.php | 7 +- src/Messages/DTO/MessagePart.php | 7 +- src/Messages/DTO/ModelMessage.php | 1 + src/Messages/DTO/SystemMessage.php | 1 + src/Messages/DTO/UserMessage.php | 1 + .../Contracts/OperationInterface.php | 8 +- src/Operations/DTO/GenerativeAiOperation.php | 5 +- src/Results/Contracts/ResultInterface.php | 8 +- src/Results/DTO/Candidate.php | 7 +- src/Results/DTO/GenerativeAiResult.php | 5 +- src/Results/DTO/TokenUsage.php | 7 +- src/Tools/DTO/FunctionCall.php | 7 +- src/Tools/DTO/FunctionDeclaration.php | 7 +- src/Tools/DTO/FunctionResponse.php | 7 +- src/Tools/DTO/Tool.php | 8 +- src/Tools/DTO/WebSearch.php | 7 +- .../Common/AbstractDataValueObjectTest.php | 556 ++++++++++++++++++ 19 files changed, 697 insertions(+), 59 deletions(-) create mode 100644 src/Common/AbstractDataValueObject.php create mode 100644 tests/unit/Common/AbstractDataValueObjectTest.php diff --git a/src/Common/AbstractDataValueObject.php b/src/Common/AbstractDataValueObject.php new file mode 100644 index 00000000..10460ebf --- /dev/null +++ b/src/Common/AbstractDataValueObject.php @@ -0,0 +1,100 @@ + + * @implements WithArrayTransformationInterface + */ +abstract class AbstractDataValueObject implements + WithArrayTransformationInterface, + WithJsonSchemaInterface, + JsonSerializable +{ + /** + * Converts the object to a JSON-serializable format. + * + * This method uses the toArray() method and then processes the result + * based on the JSON schema to ensure proper object representation for + * empty arrays. + * + * @since n.e.x.t + * + * @return mixed The JSON-serializable representation. + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $data = $this->toArray(); + $schema = static::getJsonSchema(); + + return $this->convertEmptyArraysToObjects($data, $schema); + } + + /** + * Recursively converts empty arrays to stdClass objects where the schema expects objects. + * + * @since n.e.x.t + * + * @param mixed $data The data to process. + * @param array $schema The JSON schema for the data. + * @return mixed The processed data. + */ + private function convertEmptyArraysToObjects($data, array $schema) + { + // If data is an empty array and schema expects object, convert to stdClass + if (is_array($data) && empty($data) && isset($schema['type']) && $schema['type'] === 'object') { + return new stdClass(); + } + + // If data is an array with content, recursively process nested structures + if (is_array($data)) { + // Handle object properties + if (isset($schema['properties']) && is_array($schema['properties'])) { + foreach ($data as $key => $value) { + if (isset($schema['properties'][$key]) && is_array($schema['properties'][$key])) { + $data[$key] = $this->convertEmptyArraysToObjects($value, $schema['properties'][$key]); + } + } + } + + // Handle array items + if (isset($schema['items']) && is_array($schema['items'])) { + foreach ($data as $index => $item) { + $data[$index] = $this->convertEmptyArraysToObjects($item, $schema['items']); + } + } + + // Handle oneOf schemas - just use the first one + if (isset($schema['oneOf']) && is_array($schema['oneOf'])) { + foreach ($schema['oneOf'] as $possibleSchema) { + if (is_array($possibleSchema)) { + return $this->convertEmptyArraysToObjects($data, $possibleSchema); + } + } + } + } + + return $data; + } +} diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index 0599cf5e..44f34686 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -4,8 +4,7 @@ namespace WordPress\AiClient\Files\DTO; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; +use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Files\Enums\FileTypeEnum; use WordPress\AiClient\Files\ValueObjects\MimeType; @@ -24,9 +23,9 @@ * base64Data?: string * } * - * @implements WithArrayTransformationInterface + * @extends AbstractDataValueObject */ -final class File implements WithJsonSchemaInterface, WithArrayTransformationInterface +final class File extends AbstractDataValueObject { /** * @var MimeType The MIME type of the file. diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index fe29c951..0a82c07b 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -4,8 +4,7 @@ namespace WordPress\AiClient\Messages\DTO; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; +use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; /** @@ -23,9 +22,9 @@ * parts: array * } * - * @implements WithArrayTransformationInterface + * @extends AbstractDataValueObject */ -class Message implements WithJsonSchemaInterface, WithArrayTransformationInterface +class Message extends AbstractDataValueObject { /** * @var MessageRoleEnum The role of the message sender. diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index e8bde310..dd86ab4f 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -4,8 +4,7 @@ namespace WordPress\AiClient\Messages\DTO; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; +use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Tools\DTO\FunctionCall; @@ -31,9 +30,9 @@ * functionResponse?: FunctionResponseArrayShape * } * - * @implements WithArrayTransformationInterface + * @extends AbstractDataValueObject */ -final class MessagePart implements WithJsonSchemaInterface, WithArrayTransformationInterface +final class MessagePart extends AbstractDataValueObject { /** * @var MessagePartTypeEnum The type of this message part. diff --git a/src/Messages/DTO/ModelMessage.php b/src/Messages/DTO/ModelMessage.php index f683247d..99f884cb 100644 --- a/src/Messages/DTO/ModelMessage.php +++ b/src/Messages/DTO/ModelMessage.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Messages\DTO; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; +use WordPress\AiClient\Common\Traits\HasJsonSerialization; /** * Represents a message from the AI model. diff --git a/src/Messages/DTO/SystemMessage.php b/src/Messages/DTO/SystemMessage.php index 444d2834..0997789f 100644 --- a/src/Messages/DTO/SystemMessage.php +++ b/src/Messages/DTO/SystemMessage.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Messages\DTO; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; +use WordPress\AiClient\Common\Traits\HasJsonSerialization; /** * Represents a system instruction message. diff --git a/src/Messages/DTO/UserMessage.php b/src/Messages/DTO/UserMessage.php index 28bc30bb..8ae2965a 100644 --- a/src/Messages/DTO/UserMessage.php +++ b/src/Messages/DTO/UserMessage.php @@ -5,6 +5,7 @@ namespace WordPress\AiClient\Messages\DTO; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; +use WordPress\AiClient\Common\Traits\HasJsonSerialization; /** * Represents a message from a user. diff --git a/src/Operations/Contracts/OperationInterface.php b/src/Operations/Contracts/OperationInterface.php index 1451d561..2594d0e6 100644 --- a/src/Operations/Contracts/OperationInterface.php +++ b/src/Operations/Contracts/OperationInterface.php @@ -4,8 +4,6 @@ namespace WordPress\AiClient\Operations\Contracts; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; use WordPress\AiClient\Operations\Enums\OperationStateEnum; /** @@ -15,12 +13,8 @@ * They provide a way to track the progress and retrieve results asynchronously. * * @since n.e.x.t - * - * @template TArrayShape of array - * - * @extends WithArrayTransformationInterface */ -interface OperationInterface extends WithJsonSchemaInterface, WithArrayTransformationInterface +interface OperationInterface { /** * Gets the operation ID. diff --git a/src/Operations/DTO/GenerativeAiOperation.php b/src/Operations/DTO/GenerativeAiOperation.php index f3287f11..36e91276 100644 --- a/src/Operations/DTO/GenerativeAiOperation.php +++ b/src/Operations/DTO/GenerativeAiOperation.php @@ -4,6 +4,7 @@ namespace WordPress\AiClient\Operations\DTO; +use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Operations\Contracts\OperationInterface; use WordPress\AiClient\Operations\Enums\OperationStateEnum; use WordPress\AiClient\Results\DTO\GenerativeAiResult; @@ -20,9 +21,9 @@ * * @phpstan-type GenerativeAiOperationArrayShape array{id: string, state: string, result?: GenerativeAiResultArrayShape} * - * @implements OperationInterface + * @extends AbstractDataValueObject */ -final class GenerativeAiOperation implements OperationInterface +final class GenerativeAiOperation extends AbstractDataValueObject implements OperationInterface { /** * @var string Unique identifier for this operation. diff --git a/src/Results/Contracts/ResultInterface.php b/src/Results/Contracts/ResultInterface.php index 5caa5762..09d57812 100644 --- a/src/Results/Contracts/ResultInterface.php +++ b/src/Results/Contracts/ResultInterface.php @@ -4,8 +4,6 @@ namespace WordPress\AiClient\Results\Contracts; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; use WordPress\AiClient\Results\DTO\TokenUsage; /** @@ -15,12 +13,8 @@ * such as token usage and provider-specific information. * * @since n.e.x.t - * - * @template TArrayShape of array - * - * @extends WithArrayTransformationInterface */ -interface ResultInterface extends WithJsonSchemaInterface, WithArrayTransformationInterface +interface ResultInterface { /** * Gets the result ID. diff --git a/src/Results/DTO/Candidate.php b/src/Results/DTO/Candidate.php index c05db20a..47f651c3 100644 --- a/src/Results/DTO/Candidate.php +++ b/src/Results/DTO/Candidate.php @@ -4,8 +4,7 @@ namespace WordPress\AiClient\Results\DTO; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; +use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Messages\DTO\Message; use WordPress\AiClient\Results\Enums\FinishReasonEnum; @@ -21,9 +20,9 @@ * * @phpstan-type CandidateArrayShape array{message: MessageArrayShape, finishReason: string, tokenCount: int|string} * - * @implements WithArrayTransformationInterface + * @extends AbstractDataValueObject */ -final class Candidate implements WithJsonSchemaInterface, WithArrayTransformationInterface +final class Candidate extends AbstractDataValueObject { /** * @var Message The generated message. diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index 35a619aa..cb7ceb6e 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -4,6 +4,7 @@ namespace WordPress\AiClient\Results\DTO; +use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\DTO\Message; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; @@ -27,9 +28,9 @@ * providerMetadata?: array * } * - * @implements ResultInterface + * @extends AbstractDataValueObject */ -final class GenerativeAiResult implements ResultInterface +final class GenerativeAiResult extends AbstractDataValueObject implements ResultInterface { /** * @var string Unique identifier for this result. diff --git a/src/Results/DTO/TokenUsage.php b/src/Results/DTO/TokenUsage.php index faf9ac45..233405ba 100644 --- a/src/Results/DTO/TokenUsage.php +++ b/src/Results/DTO/TokenUsage.php @@ -4,8 +4,7 @@ namespace WordPress\AiClient\Results\DTO; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; +use WordPress\AiClient\Common\AbstractDataValueObject; /** * Represents token usage statistics for an AI operation. @@ -21,9 +20,9 @@ * totalTokens: int|string * } * - * @implements WithArrayTransformationInterface + * @extends AbstractDataValueObject */ -final class TokenUsage implements WithJsonSchemaInterface, WithArrayTransformationInterface +final class TokenUsage extends AbstractDataValueObject { /** * @var int Number of tokens in the prompt. diff --git a/src/Tools/DTO/FunctionCall.php b/src/Tools/DTO/FunctionCall.php index 5d40fb84..c193444a 100644 --- a/src/Tools/DTO/FunctionCall.php +++ b/src/Tools/DTO/FunctionCall.php @@ -4,8 +4,7 @@ namespace WordPress\AiClient\Tools\DTO; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; +use WordPress\AiClient\Common\AbstractDataValueObject; /** * Represents a function call request from an AI model. @@ -17,9 +16,9 @@ * * @phpstan-type FunctionCallArrayShape array{id?: string, name?: string, args?: mixed} * - * @implements WithArrayTransformationInterface + * @extends AbstractDataValueObject */ -final class FunctionCall implements WithJsonSchemaInterface, WithArrayTransformationInterface +final class FunctionCall extends AbstractDataValueObject { /** * @var string|null Unique identifier for this function call. diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index 02631b2a..1c805d54 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -4,8 +4,7 @@ namespace WordPress\AiClient\Tools\DTO; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; +use WordPress\AiClient\Common\AbstractDataValueObject; /** * Represents a function declaration for AI models. @@ -17,9 +16,9 @@ * * @phpstan-type FunctionDeclarationArrayShape array{name: string, description: string, parameters?: mixed} * - * @implements WithArrayTransformationInterface + * @extends AbstractDataValueObject */ -final class FunctionDeclaration implements WithJsonSchemaInterface, WithArrayTransformationInterface +final class FunctionDeclaration extends AbstractDataValueObject { /** * @var string The name of the function. diff --git a/src/Tools/DTO/FunctionResponse.php b/src/Tools/DTO/FunctionResponse.php index 686de175..3557a9d3 100644 --- a/src/Tools/DTO/FunctionResponse.php +++ b/src/Tools/DTO/FunctionResponse.php @@ -4,8 +4,7 @@ namespace WordPress\AiClient\Tools\DTO; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; +use WordPress\AiClient\Common\AbstractDataValueObject; /** * Represents a response to a function call. @@ -17,9 +16,9 @@ * * @phpstan-type FunctionResponseArrayShape array{id: string, name: string, response: mixed} * - * @implements WithArrayTransformationInterface + * @extends AbstractDataValueObject */ -final class FunctionResponse implements WithJsonSchemaInterface, WithArrayTransformationInterface +final class FunctionResponse extends AbstractDataValueObject { /** * @var string The ID of the function call this is responding to. diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index 41fc020b..a88cec5a 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -4,8 +4,7 @@ namespace WordPress\AiClient\Tools\DTO; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; +use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Providers\Enums\ToolTypeEnum; /** @@ -25,9 +24,9 @@ * webSearch?: WebSearchArrayShape * } * - * @implements WithArrayTransformationInterface + * @extends AbstractDataValueObject */ -final class Tool implements WithJsonSchemaInterface, WithArrayTransformationInterface +final class Tool extends AbstractDataValueObject { /** * @var ToolTypeEnum The type of tool. @@ -67,7 +66,6 @@ public function __construct($content) } } - /** * Gets the tool type. * diff --git a/src/Tools/DTO/WebSearch.php b/src/Tools/DTO/WebSearch.php index 9b059c28..2d7c2b42 100644 --- a/src/Tools/DTO/WebSearch.php +++ b/src/Tools/DTO/WebSearch.php @@ -4,8 +4,7 @@ namespace WordPress\AiClient\Tools\DTO; -use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; -use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; +use WordPress\AiClient\Common\AbstractDataValueObject; /** * Represents web search configuration for AI models. @@ -17,9 +16,9 @@ * * @phpstan-type WebSearchArrayShape array{allowedDomains?: string[], disallowedDomains?: string[]} * - * @implements WithArrayTransformationInterface + * @extends AbstractDataValueObject */ -final class WebSearch implements WithJsonSchemaInterface, WithArrayTransformationInterface +final class WebSearch extends AbstractDataValueObject { /** * @var string[] List of domains that are allowed for web search. diff --git a/tests/unit/Common/AbstractDataValueObjectTest.php b/tests/unit/Common/AbstractDataValueObjectTest.php new file mode 100644 index 00000000..e89a9d20 --- /dev/null +++ b/tests/unit/Common/AbstractDataValueObjectTest.php @@ -0,0 +1,556 @@ + [], + 'nonEmptyObject' => ['key' => 'value'], + 'emptyArray' => [], + 'nonEmptyArray' => [1, 2, 3], + ]; + } + + public static function fromArray(array $array) + { + return new static(); + } + + public static function getJsonSchema(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'emptyObject' => [ + 'type' => 'object', + 'properties' => [] + ], + 'nonEmptyObject' => [ + 'type' => 'object', + 'properties' => [ + 'key' => ['type' => 'string'] + ] + ], + 'emptyArray' => [ + 'type' => 'array', + 'items' => ['type' => 'integer'] + ], + 'nonEmptyArray' => [ + 'type' => 'array', + 'items' => ['type' => 'integer'] + ], + ], + ]; + } + }; + + $result = $testObject->jsonSerialize(); + + // Verify result is an array + $this->assertIsArray($result); + + // Empty array marked as object in schema should be stdClass + $this->assertInstanceOf(stdClass::class, $result['emptyObject']); + + // Non-empty object should remain array + $this->assertIsArray($result['nonEmptyObject']); + $this->assertEquals(['key' => 'value'], $result['nonEmptyObject']); + + // Empty array marked as array in schema should remain array + $this->assertIsArray($result['emptyArray']); + $this->assertEmpty($result['emptyArray']); + + // Non-empty array should remain array + $this->assertIsArray($result['nonEmptyArray']); + $this->assertEquals([1, 2, 3], $result['nonEmptyArray']); + + // Verify JSON encoding produces correct output + $json = json_encode($result); + $this->assertIsString($json); + $decoded = json_decode($json, true); + + // In JSON, empty object should be {} not [] + $this->assertStringContainsString('"emptyObject":{}', $json); + $this->assertStringContainsString('"emptyArray":[]', $json); + } + + /** + * Tests nested object conversion. + * + * @return void + */ + public function testNestedObjectConversion(): void + { + $testObject = new class extends AbstractDataValueObject { + public function toArray(): array + { + return [ + 'nested' => [ + 'emptyChild' => [], + 'nonEmptyChild' => ['value' => 'test'], + ], + ]; + } + + public static function fromArray(array $array) + { + return new static(); + } + + public static function getJsonSchema(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'nested' => [ + 'type' => 'object', + 'properties' => [ + 'emptyChild' => [ + 'type' => 'object', + 'properties' => [] + ], + 'nonEmptyChild' => [ + 'type' => 'object', + 'properties' => [ + 'value' => ['type' => 'string'] + ] + ], + ], + ], + ], + ]; + } + }; + + $result = $testObject->jsonSerialize(); + + // Verify result is an array + $this->assertIsArray($result); + $this->assertIsArray($result['nested']); + $this->assertInstanceOf(stdClass::class, $result['nested']['emptyChild']); + $this->assertIsArray($result['nested']['nonEmptyChild']); + + $json = json_encode($result); + $this->assertIsString($json); + $this->assertStringContainsString('"emptyChild":{}', $json); + } + + /** + * Tests handling of oneOf schemas uses first schema without validation. + * + * @return void + */ + public function testOneOfSchemaHandling(): void + { + $testObject = new class extends AbstractDataValueObject { + public function toArray(): array + { + return [ + 'dynamicField' => [ + 'type' => 'objectType', + 'data' => [], + ], + ]; + } + + public static function fromArray(array $array) + { + return new static(); + } + + public static function getJsonSchema(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'dynamicField' => [ + 'oneOf' => [ + [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + 'const' => 'objectType' + ], + 'data' => [ + 'type' => 'object', + 'properties' => [] + ], + ], + 'required' => ['type', 'data'], + ], + [ + 'type' => 'object', + 'properties' => [ + 'type' => [ + 'type' => 'string', + 'const' => 'arrayType' + ], + 'data' => [ + 'type' => 'array', + 'items' => ['type' => 'string'] + ], + ], + 'required' => ['type', 'data'], + ], + ], + ], + ], + ]; + } + }; + + $result = $testObject->jsonSerialize(); + + // Verify result is an array + $this->assertIsArray($result); + + // The implementation uses the first oneOf schema without validation + // Since the first schema has 'data' as type 'object', empty array is converted + $this->assertIsArray($result['dynamicField']); + $this->assertInstanceOf(stdClass::class, $result['dynamicField']['data']); + + $json = json_encode($result); + $this->assertIsString($json); + $this->assertStringContainsString('"data":{}', $json); + } + + /** + * Tests that arrays of objects are processed recursively. + * + * @return void + */ + public function testArrayOfObjectsProcessing(): void + { + $testObject = new class extends AbstractDataValueObject { + public function toArray(): array + { + return [ + 'items' => [ + ['name' => 'Item 1', 'metadata' => []], + ['name' => 'Item 2', 'metadata' => ['key' => 'value']], + ['name' => 'Item 3', 'metadata' => []], + ], + ]; + } + + public static function fromArray(array $array) + { + return new static(); + } + + public static function getJsonSchema(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'items' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'metadata' => [ + 'type' => 'object', + 'properties' => [] + ], + ], + ], + ], + ], + ]; + } + }; + + $result = $testObject->jsonSerialize(); + + // Verify result is an array + $this->assertIsArray($result); + + // Verify array structure is preserved + $this->assertIsArray($result['items']); + $this->assertCount(3, $result['items']); + + // Each item should have empty metadata converted to stdClass + $items = $result['items']; + $this->assertIsArray($items[0]); + $this->assertInstanceOf(stdClass::class, $items[0]['metadata']); + $this->assertIsArray($items[1]); + $this->assertIsArray($items[1]['metadata']); // Non-empty remains array + $this->assertIsArray($items[2]); + $this->assertInstanceOf(stdClass::class, $items[2]['metadata']); + + $json = json_encode($result); + $this->assertIsString($json); + $this->assertStringContainsString('"metadata":{}', $json); + $this->assertStringContainsString('"metadata":{"key":"value"}', $json); + } + + /** + * Tests deeply nested structure conversion. + * + * @return void + */ + public function testDeeplyNestedStructures(): void + { + $testObject = new class extends AbstractDataValueObject { + public function toArray(): array + { + return [ + 'level1' => [ + 'level2' => [ + 'level3' => [ + 'emptyObject' => [], + 'emptyArray' => [], + ], + ], + ], + ]; + } + + public static function fromArray(array $array) + { + return new static(); + } + + public static function getJsonSchema(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'level1' => [ + 'type' => 'object', + 'properties' => [ + 'level2' => [ + 'type' => 'object', + 'properties' => [ + 'level3' => [ + 'type' => 'object', + 'properties' => [ + 'emptyObject' => [ + 'type' => 'object', + 'properties' => [] + ], + 'emptyArray' => [ + 'type' => 'array', + 'items' => ['type' => 'string'] + ], + ], + ], + ], + ], + ], + ], + ], + ]; + } + }; + + $result = $testObject->jsonSerialize(); + + // Verify result is an array + $this->assertIsArray($result); + + // Verify deep nesting is preserved + $this->assertIsArray($result['level1']); + $this->assertIsArray($result['level1']['level2']); + $this->assertIsArray($result['level1']['level2']['level3']); + + // Verify conversions at deepest level + $this->assertInstanceOf(stdClass::class, $result['level1']['level2']['level3']['emptyObject']); + $this->assertIsArray($result['level1']['level2']['level3']['emptyArray']); + $this->assertEmpty($result['level1']['level2']['level3']['emptyArray']); + } + + /** + * Tests that non-array data types pass through unchanged. + * + * @return void + */ + public function testNonArrayDataPassesThrough(): void + { + $testObject = new class extends AbstractDataValueObject { + public function toArray(): array + { + return [ + 'string' => 'test', + 'number' => 42, + 'float' => 3.14, + 'boolean' => true, + 'null' => null, + 'mixedObject' => [ + 'value' => 'test', + 'emptyData' => [], + ], + ]; + } + + public static function fromArray(array $array) + { + return new static(); + } + + public static function getJsonSchema(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'string' => ['type' => 'string'], + 'number' => ['type' => 'integer'], + 'float' => ['type' => 'number'], + 'boolean' => ['type' => 'boolean'], + 'null' => ['type' => 'null'], + 'mixedObject' => [ + 'type' => 'object', + 'properties' => [ + 'value' => ['type' => 'string'], + 'emptyData' => [ + 'type' => 'object', + 'properties' => [] + ], + ], + ], + ], + ]; + } + }; + + $result = $testObject->jsonSerialize(); + + // Verify result is an array + $this->assertIsArray($result); + + // Non-array values should pass through unchanged + $this->assertSame('test', $result['string']); + $this->assertSame(42, $result['number']); + $this->assertSame(3.14, $result['float']); + $this->assertSame(true, $result['boolean']); + $this->assertNull($result['null']); + + // Mixed object should have empty array converted + $this->assertIsArray($result['mixedObject']); + $this->assertSame('test', $result['mixedObject']['value']); + $this->assertInstanceOf(stdClass::class, $result['mixedObject']['emptyData']); + } + + /** + * Tests behavior when schema is missing or incomplete. + * + * @return void + */ + public function testMissingSchemaProperties(): void + { + $testObject = new class extends AbstractDataValueObject { + public function toArray(): array + { + return [ + 'hasSchema' => [], + 'noSchema' => [], + ]; + } + + public static function fromArray(array $array) + { + return new static(); + } + + public static function getJsonSchema(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'hasSchema' => [ + 'type' => 'object', + 'properties' => [] + ], + // 'noSchema' is intentionally missing from properties + ], + ]; + } + }; + + $result = $testObject->jsonSerialize(); + + // Verify result is an array + $this->assertIsArray($result); + + // Property with schema should be converted + $this->assertInstanceOf(stdClass::class, $result['hasSchema']); + + // Property without schema should remain as-is + $this->assertIsArray($result['noSchema']); + $this->assertEmpty($result['noSchema']); + + $json = json_encode($result); + $this->assertIsString($json); + $this->assertStringContainsString('"hasSchema":{}', $json); + $this->assertStringContainsString('"noSchema":[]', $json); + } + + /** + * Tests that AbstractDataValueObject implements all required interfaces. + * + * @return void + */ + public function testImplementsRequiredInterfaces(): void + { + $testObject = new class extends AbstractDataValueObject { + public function toArray(): array + { + return ['test' => 'value']; + } + + public static function fromArray(array $array) + { + return new static(); + } + + public static function getJsonSchema(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'test' => ['type' => 'string'] + ], + ]; + } + }; + + // Verify interface implementations + $this->assertInstanceOf(\WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface::class, $testObject); + $this->assertInstanceOf(\WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface::class, $testObject); + $this->assertInstanceOf(\JsonSerializable::class, $testObject); + + // Verify methods exist and work + $this->assertIsArray($testObject->toArray()); + $this->assertIsArray($testObject::getJsonSchema()); + $this->assertNotNull($testObject->jsonSerialize()); + } +} \ No newline at end of file From f17f6627a28d55e9f53b1d3c298b0025ffc59dde Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 15:39:23 -0600 Subject: [PATCH 11/36] refactor: simplifies File::fromArray --- src/Files/DTO/File.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index 44f34686..35ac1fdc 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -428,11 +428,7 @@ public static function fromArray(array $array): File if (!isset($array['mimeType']) || !isset($array['base64Data'])) { throw new \InvalidArgumentException('Inline file requires mimeType and base64Data.'); } - // Create data URI from base64 data and mime type - $mimeType = $array['mimeType']; - $base64Data = $array['base64Data']; - $dataUri = sprintf('data:%s;base64,%s', $mimeType, $base64Data); - return new self($dataUri); + return new self($array['base64Data'], $array['mimeType']); } } } From 1f77a32ac3b5ff44f964aa6861915f699856b166 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 15:44:39 -0600 Subject: [PATCH 12/36] refactor: simplifies Message::fromArray --- src/Messages/DTO/Message.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index 0a82c07b..fd5e8caa 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -122,7 +122,7 @@ public function toArray(): array * * @since n.e.x.t * - * @return self|UserMessage|ModelMessage|SystemMessage + * @return UserMessage|ModelMessage|SystemMessage */ final public static function fromArray(array $array): Message { @@ -137,10 +137,9 @@ final public static function fromArray(array $array): Message return new UserMessage($parts); } elseif ($role->isModel()) { return new ModelMessage($parts); - } elseif ($role->isSystem()) { + } else { + // System is the only remaining option return new SystemMessage($parts); } - - return new self($role, $parts); } } From 3bc7064f30d8504e20cd36bdf865be1284f47436 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 15:46:51 -0600 Subject: [PATCH 13/36] refactor: simplifies MessagePart::fromArray --- src/Messages/DTO/MessagePart.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index dd86ab4f..a30ef582 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -267,14 +267,13 @@ public static function fromArray(array $array): MessagePart } $functionCallData = $array['functionCall']; return new self(FunctionCall::fromArray($functionCallData)); - } elseif ($type->isFunctionResponse()) { + } else { + // Function response is the only remaining option if (!isset($array['functionResponse'])) { throw new \InvalidArgumentException('Function response message part requires functionResponse field.'); } $functionResponseData = $array['functionResponse']; return new self(FunctionResponse::fromArray($functionResponseData)); } - - throw new \InvalidArgumentException(sprintf('Unknown message part type: %s', $array['type'])); } } From a9629c445ce024d425f8b71cbd1b8138ab9a9976 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 15:50:32 -0600 Subject: [PATCH 14/36] refactor: simplifies GenerativeAiResult --- src/Results/DTO/GenerativeAiResult.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index cb7ceb6e..3038c108 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -7,7 +7,6 @@ use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\DTO\Message; -use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Results\Contracts\ResultInterface; /** @@ -403,9 +402,7 @@ public function toArray(): array { return [ 'id' => $this->id, - 'candidates' => array_map(function (Candidate $candidate) { - return $candidate->toArray(); - }, $this->candidates), + 'candidates' => array_map(fn(Candidate $candidate) => $candidate->toArray(), $this->candidates), 'tokenUsage' => $this->tokenUsage->toArray(), 'providerMetadata' => $this->providerMetadata, ]; @@ -419,9 +416,7 @@ public function toArray(): array public static function fromArray(array $array): GenerativeAiResult { $candidatesData = $array['candidates']; - $candidates = array_map(function (array $candidateData) { - return Candidate::fromArray($candidateData); - }, $candidatesData); + $candidates = array_map(fn(array $candidateData) => Candidate::fromArray($candidateData), $candidatesData); $tokenUsageData = $array['tokenUsage']; $providerMetadata = $array['providerMetadata'] ?? []; From 0f8f66df4a47c2badf53251c9f7afadda484693c Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 15:53:02 -0600 Subject: [PATCH 15/36] fix: corrects incorrect types --- src/Results/DTO/Candidate.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Results/DTO/Candidate.php b/src/Results/DTO/Candidate.php index 47f651c3..2f984606 100644 --- a/src/Results/DTO/Candidate.php +++ b/src/Results/DTO/Candidate.php @@ -18,7 +18,7 @@ * * @phpstan-import-type MessageArrayShape from Message * - * @phpstan-type CandidateArrayShape array{message: MessageArrayShape, finishReason: string, tokenCount: int|string} + * @phpstan-type CandidateArrayShape array{message: MessageArrayShape, finishReason: string, tokenCount: int} * * @extends AbstractDataValueObject */ @@ -150,7 +150,7 @@ public static function fromArray(array $array): Candidate return new self( Message::fromArray($messageData), FinishReasonEnum::from($array['finishReason']), - (int) $array['tokenCount'] + $array['tokenCount'] ); } } From 18afbb5aeab7d561fc6c288e5db0b988d48bd39b Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 15:56:14 -0600 Subject: [PATCH 16/36] refactor: simplifies GenerativeAiResult::fromArray --- src/Results/DTO/GenerativeAiResult.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index 3038c108..f54296ad 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -415,17 +415,13 @@ public function toArray(): array */ public static function fromArray(array $array): GenerativeAiResult { - $candidatesData = $array['candidates']; - $candidates = array_map(fn(array $candidateData) => Candidate::fromArray($candidateData), $candidatesData); - - $tokenUsageData = $array['tokenUsage']; - $providerMetadata = $array['providerMetadata'] ?? []; + $candidates = array_map(fn(array $candidateData) => Candidate::fromArray($candidateData), $array['candidates']); return new self( $array['id'], $candidates, - TokenUsage::fromArray($tokenUsageData), - $providerMetadata + TokenUsage::fromArray($array['tokenUsage']), + $array['providerMetadata'] ?? [] ); } } From a66a56bda40fd87638d8fa963d66c19335e56c99 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 15:57:50 -0600 Subject: [PATCH 17/36] fix: corrects TokenUsage types --- src/Results/DTO/TokenUsage.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Results/DTO/TokenUsage.php b/src/Results/DTO/TokenUsage.php index 233405ba..f0710bd2 100644 --- a/src/Results/DTO/TokenUsage.php +++ b/src/Results/DTO/TokenUsage.php @@ -15,9 +15,9 @@ * @since n.e.x.t * * @phpstan-type TokenUsageArrayShape array{ - * promptTokens: int|string, - * completionTokens: int|string, - * totalTokens: int|string + * promptTokens: int, + * completionTokens: int, + * totalTokens: int * } * * @extends AbstractDataValueObject @@ -142,9 +142,9 @@ public function toArray(): array public static function fromArray(array $array): TokenUsage { return new self( - (int) $array['promptTokens'], - (int) $array['completionTokens'], - (int) $array['totalTokens'] + $array['promptTokens'], + $array['completionTokens'], + $array['totalTokens'] ); } } From 5bef6fb1e8e3a4a323c0dd9bf97bb5b93246412a Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 16:01:39 -0600 Subject: [PATCH 18/36] refactor: simplifies FunctionCall --- src/Tools/DTO/FunctionCall.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Tools/DTO/FunctionCall.php b/src/Tools/DTO/FunctionCall.php index c193444a..59ceca6f 100644 --- a/src/Tools/DTO/FunctionCall.php +++ b/src/Tools/DTO/FunctionCall.php @@ -14,7 +14,7 @@ * * @since n.e.x.t * - * @phpstan-type FunctionCallArrayShape array{id?: string, name?: string, args?: mixed} + * @phpstan-type FunctionCallArrayShape array{id?: string, name?: string, args?: array} * * @extends AbstractDataValueObject */ @@ -163,13 +163,10 @@ public function toArray(): array */ public static function fromArray(array $array): FunctionCall { - /** @var array $args */ - $args = $array['args'] ?? []; - return new self( - isset($array['id']) ? $array['id'] : null, - isset($array['name']) ? $array['name'] : null, - $args + $array['id'] ?? null, + $array['name'] ?? null, + $array['args'] ?? [] ); } } From eac51461df83c9db0602f877b1e155da216dfdfd Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 16:05:43 -0600 Subject: [PATCH 19/36] refactor: simplifies Tool::fromArray --- src/Tools/DTO/Tool.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index a88cec5a..f31cbdcc 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -183,14 +183,13 @@ public static function fromArray(array $array): Tool return FunctionDeclaration::fromArray($declarationData); }, $declarationsData); return new self($declarations); - } elseif ($type->isWebSearch()) { + } else { + // Web search is the only remaining option if (!isset($array['webSearch'])) { throw new \InvalidArgumentException('Web search tool requires webSearch field.'); } $webSearchData = $array['webSearch']; return new self(WebSearch::fromArray($webSearchData)); } - - throw new \InvalidArgumentException(sprintf('Unknown tool type: %s', $array['type'])); } } From 28115978c6155c27492e831cd47dca62d79c6cfa Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 28 Jul 2025 16:06:48 -0600 Subject: [PATCH 20/36] refactor: simplifies WebSearch --- src/Tools/DTO/WebSearch.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Tools/DTO/WebSearch.php b/src/Tools/DTO/WebSearch.php index 2d7c2b42..373ff36d 100644 --- a/src/Tools/DTO/WebSearch.php +++ b/src/Tools/DTO/WebSearch.php @@ -119,12 +119,9 @@ public function toArray(): array */ public static function fromArray(array $array): WebSearch { - $allowedDomains = $array['allowedDomains'] ?? []; - $disallowedDomains = $array['disallowedDomains'] ?? []; - return new self( - $allowedDomains, - $disallowedDomains + $array['allowedDomains'] ?? [], + $array['disallowedDomains'] ?? [] ); } } From 3e36f0fabc7a4147e6551bf159579f671e62bcca Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 09:31:55 -0600 Subject: [PATCH 21/36] refactor: simplifies Fiel and MessagePart toArray --- src/Files/DTO/File.php | 4 +++- src/Messages/DTO/MessagePart.php | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index 35ac1fdc..58509c14 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -401,10 +401,12 @@ public function toArray(): array 'mimeType' => $this->getMimeType(), ]; - if ($this->fileType->isRemote() && $this->url !== null) { + if ($this->url !== null) { $data['url'] = $this->url; } elseif (!$this->fileType->isRemote() && $this->base64Data !== null) { $data['base64Data'] = $this->base64Data; + } else { + throw new RuntimeException('File requires either url or base64Data. This should not be a possible condition.'); } return $data; diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index a30ef582..12e0fca3 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -4,6 +4,8 @@ namespace WordPress\AiClient\Messages\DTO; +use InvalidArgumentException; +use RuntimeException; use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; @@ -228,14 +230,16 @@ public function toArray(): array { $data = ['type' => $this->type->value]; - if ($this->type->isText() && $this->text !== null) { + if ($this->text !== null) { $data['text'] = $this->text; - } elseif ($this->type->isFile() && $this->file !== null) { + } elseif ($this->file !== null) { $data['file'] = $this->file->toArray(); - } elseif ($this->type->isFunctionCall() && $this->functionCall !== null) { + } elseif ($this->functionCall !== null) { $data['functionCall'] = $this->functionCall->toArray(); - } elseif ($this->type->isFunctionResponse() && $this->functionResponse !== null) { + } elseif ($this->functionResponse !== null) { $data['functionResponse'] = $this->functionResponse->toArray(); + } else { + throw new RuntimeException('MessagePart requires one of: text, file, functionCall, or functionResponse. This should not be a possible condition.'); } return $data; From b6890c825a8e60c57a70af3cf6e1d6b7aea4eebe Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 10:03:12 -0600 Subject: [PATCH 22/36] chore: corrects too specific Message return type --- src/Messages/DTO/Message.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index fd5e8caa..0d896d65 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -122,7 +122,7 @@ public function toArray(): array * * @since n.e.x.t * - * @return UserMessage|ModelMessage|SystemMessage + * @return Message The specific message class based on the role. */ final public static function fromArray(array $array): Message { From cb985cca1bb37ecdc2d0947671bab8bbf58629ff Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 10:28:07 -0600 Subject: [PATCH 23/36] fix: adds missing imports --- src/Files/DTO/File.php | 6 +++++- src/Messages/DTO/MessagePart.php | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index 58509c14..5a1c9208 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -4,6 +4,8 @@ namespace WordPress\AiClient\Files\DTO; +use InvalidArgumentException; +use RuntimeException; use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Files\Enums\FileTypeEnum; use WordPress\AiClient\Files\ValueObjects\MimeType; @@ -406,7 +408,9 @@ public function toArray(): array } elseif (!$this->fileType->isRemote() && $this->base64Data !== null) { $data['base64Data'] = $this->base64Data; } else { - throw new RuntimeException('File requires either url or base64Data. This should not be a possible condition.'); + throw new RuntimeException( + 'File requires either url or base64Data. This should not be a possible condition.' + ); } return $data; diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index 12e0fca3..dff9e3b1 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -239,7 +239,10 @@ public function toArray(): array } elseif ($this->functionResponse !== null) { $data['functionResponse'] = $this->functionResponse->toArray(); } else { - throw new RuntimeException('MessagePart requires one of: text, file, functionCall, or functionResponse. This should not be a possible condition.'); + throw new RuntimeException( + 'MessagePart requires one of: text, file, functionCall, or functionResponse. ' + . 'This should not be a possible condition.' + ); } return $data; From 25f0491831e4d44acc62c5584e5190a1c4201941 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 12:45:39 -0600 Subject: [PATCH 24/36] feat: adds fromArray key validation --- src/Common/AbstractDataValueObject.php | 31 ++++++++++++++ src/Files/DTO/File.php | 19 ++++----- src/Messages/DTO/Message.php | 3 ++ src/Messages/DTO/MessagePart.php | 44 +++++++------------- src/Operations/DTO/GenerativeAiOperation.php | 3 ++ src/Results/DTO/Candidate.php | 2 + src/Results/DTO/GenerativeAiResult.php | 2 + src/Results/DTO/TokenUsage.php | 2 + src/Tools/DTO/FunctionDeclaration.php | 3 ++ src/Tools/DTO/FunctionResponse.php | 2 + src/Tools/DTO/Tool.php | 23 +++++----- 11 files changed, 82 insertions(+), 52 deletions(-) diff --git a/src/Common/AbstractDataValueObject.php b/src/Common/AbstractDataValueObject.php index 10460ebf..82d99c5b 100644 --- a/src/Common/AbstractDataValueObject.php +++ b/src/Common/AbstractDataValueObject.php @@ -4,6 +4,7 @@ namespace WordPress\AiClient\Common; +use InvalidArgumentException; use JsonSerializable; use stdClass; use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; @@ -31,6 +32,36 @@ abstract class AbstractDataValueObject implements WithJsonSchemaInterface, JsonSerializable { + /** + * Validates that required keys exist in the array data. + * + * @since n.e.x.t + * + * @param TArrayShape $data The array data to validate. + * @param string[] $requiredKeys The keys that must be present. + * @throws InvalidArgumentException If any required key is missing. + */ + protected static function validateFromArrayData(array $data, array $requiredKeys): void + { + $missingKeys = []; + + foreach ($requiredKeys as $key) { + if (!isset($data[$key])) { + $missingKeys[] = $key; + } + } + + if (!empty($missingKeys)) { + throw new InvalidArgumentException( + sprintf( + '%s::fromArray() missing required keys: %s', + static::class, + implode(', ', $missingKeys) + ) + ); + } + } + /** * Converts the object to a JSON-serializable format. * diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index 5a1c9208..e71102d3 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -423,18 +423,17 @@ public function toArray(): array */ public static function fromArray(array $array): File { - $fileType = FileTypeEnum::from($array['fileType']); + static::validateFromArrayData($array, ['fileType']); - if ($fileType->isRemote()) { - if (!isset($array['url']) || !isset($array['mimeType'])) { - throw new \InvalidArgumentException('Remote file requires url and mimeType.'); - } - return new self($array['url'], $array['mimeType']); + // Check which properties are set to determine how to construct the File + $mimeType = $array['mimeType'] ?? null; + + if (isset($array['url'])) { + return new self($array['url'], $mimeType); + } elseif (isset($array['base64Data'])) { + return new self($array['base64Data'], $mimeType); } else { - if (!isset($array['mimeType']) || !isset($array['base64Data'])) { - throw new \InvalidArgumentException('Inline file requires mimeType and base64Data.'); - } - return new self($array['base64Data'], $array['mimeType']); + throw new InvalidArgumentException('File requires either url or base64Data.'); } } } diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index 0d896d65..c0983d7d 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -4,6 +4,7 @@ namespace WordPress\AiClient\Messages\DTO; +use InvalidArgumentException; use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; @@ -126,6 +127,8 @@ public function toArray(): array */ final public static function fromArray(array $array): Message { + static::validateFromArrayData($array, ['role', 'parts']); + $role = MessageRoleEnum::from($array['role']); $partsData = $array['parts']; $parts = array_map(function (array $partData) { diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index dff9e3b1..98daccf3 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -67,7 +67,7 @@ final class MessagePart extends AbstractDataValueObject * @since n.e.x.t * * @param mixed $content The content of this message part. - * @throws \InvalidArgumentException If an unsupported content type is provided. + * @throws InvalidArgumentException If an unsupported content type is provided. */ public function __construct($content) { @@ -85,7 +85,7 @@ public function __construct($content) $this->functionResponse = $content; } else { $type = is_object($content) ? get_class($content) : gettype($content); - throw new \InvalidArgumentException( + throw new InvalidArgumentException( sprintf( 'Unsupported content type %s. Expected string, File, ' . 'FunctionCall, or FunctionResponse.', @@ -239,10 +239,7 @@ public function toArray(): array } elseif ($this->functionResponse !== null) { $data['functionResponse'] = $this->functionResponse->toArray(); } else { - throw new RuntimeException( - 'MessagePart requires one of: text, file, functionCall, or functionResponse. ' - . 'This should not be a possible condition.' - ); + throw new RuntimeException('MessagePart requires one of: text, file, functionCall, or functionResponse. This should not be a possible condition.'); } return $data; @@ -255,32 +252,21 @@ public function toArray(): array */ public static function fromArray(array $array): MessagePart { - $type = MessagePartTypeEnum::from($array['type']); + static::validateFromArrayData($array, ['type']); - if ($type->isText()) { - if (!isset($array['text'])) { - throw new \InvalidArgumentException('Text message part requires text field.'); - } + // Check which properties are set to determine how to construct the MessagePart + if (isset($array['text'])) { return new self($array['text']); - } elseif ($type->isFile()) { - if (!isset($array['file'])) { - throw new \InvalidArgumentException('File message part requires file field.'); - } - $fileData = $array['file']; - return new self(File::fromArray($fileData)); - } elseif ($type->isFunctionCall()) { - if (!isset($array['functionCall'])) { - throw new \InvalidArgumentException('Function call message part requires functionCall field.'); - } - $functionCallData = $array['functionCall']; - return new self(FunctionCall::fromArray($functionCallData)); + } elseif (isset($array['file'])) { + return new self(File::fromArray($array['file'])); + } elseif (isset($array['functionCall'])) { + return new self(FunctionCall::fromArray($array['functionCall'])); + } elseif (isset($array['functionResponse'])) { + return new self(FunctionResponse::fromArray($array['functionResponse'])); } else { - // Function response is the only remaining option - if (!isset($array['functionResponse'])) { - throw new \InvalidArgumentException('Function response message part requires functionResponse field.'); - } - $functionResponseData = $array['functionResponse']; - return new self(FunctionResponse::fromArray($functionResponseData)); + throw new InvalidArgumentException( + 'MessagePart requires one of: text, file, functionCall, or functionResponse.' + ); } } } diff --git a/src/Operations/DTO/GenerativeAiOperation.php b/src/Operations/DTO/GenerativeAiOperation.php index 36e91276..d0df88ec 100644 --- a/src/Operations/DTO/GenerativeAiOperation.php +++ b/src/Operations/DTO/GenerativeAiOperation.php @@ -4,6 +4,7 @@ namespace WordPress\AiClient\Operations\DTO; +use InvalidArgumentException; use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Operations\Contracts\OperationInterface; use WordPress\AiClient\Operations\Enums\OperationStateEnum; @@ -168,6 +169,8 @@ public function toArray(): array */ public static function fromArray(array $array): GenerativeAiOperation { + static::validateFromArrayData($array, ['id', 'state']); + $state = OperationStateEnum::from($array['state']); $result = null; if (isset($array['result'])) { diff --git a/src/Results/DTO/Candidate.php b/src/Results/DTO/Candidate.php index 2f984606..20ab4211 100644 --- a/src/Results/DTO/Candidate.php +++ b/src/Results/DTO/Candidate.php @@ -145,6 +145,8 @@ public function toArray(): array */ public static function fromArray(array $array): Candidate { + static::validateFromArrayData($array, ['message', 'finishReason', 'tokenCount']); + $messageData = $array['message']; return new self( diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index f54296ad..f211fedf 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -415,6 +415,8 @@ public function toArray(): array */ public static function fromArray(array $array): GenerativeAiResult { + static::validateFromArrayData($array, ['id', 'candidates', 'tokenUsage']); + $candidates = array_map(fn(array $candidateData) => Candidate::fromArray($candidateData), $array['candidates']); return new self( diff --git a/src/Results/DTO/TokenUsage.php b/src/Results/DTO/TokenUsage.php index f0710bd2..5e37a5f8 100644 --- a/src/Results/DTO/TokenUsage.php +++ b/src/Results/DTO/TokenUsage.php @@ -141,6 +141,8 @@ public function toArray(): array */ public static function fromArray(array $array): TokenUsage { + static::validateFromArrayData($array, ['promptTokens', 'completionTokens', 'totalTokens']); + return new self( $array['promptTokens'], $array['completionTokens'], diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index 1c805d54..1374bd0c 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -4,6 +4,7 @@ namespace WordPress\AiClient\Tools\DTO; +use InvalidArgumentException; use WordPress\AiClient\Common\AbstractDataValueObject; /** @@ -142,6 +143,8 @@ public function toArray(): array */ public static function fromArray(array $array): FunctionDeclaration { + static::validateFromArrayData($array, ['name', 'description']); + return new self( $array['name'], $array['description'], diff --git a/src/Tools/DTO/FunctionResponse.php b/src/Tools/DTO/FunctionResponse.php index 3557a9d3..9f368b17 100644 --- a/src/Tools/DTO/FunctionResponse.php +++ b/src/Tools/DTO/FunctionResponse.php @@ -137,6 +137,8 @@ public function toArray(): array */ public static function fromArray(array $array): FunctionResponse { + static::validateFromArrayData($array, ['id', 'name', 'response']); + return new self( $array['id'], $array['name'], diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index f31cbdcc..a6d3f2a7 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -4,6 +4,7 @@ namespace WordPress\AiClient\Tools\DTO; +use InvalidArgumentException; use WordPress\AiClient\Common\AbstractDataValueObject; use WordPress\AiClient\Providers\Enums\ToolTypeEnum; @@ -172,24 +173,20 @@ public function toArray(): array */ public static function fromArray(array $array): Tool { - $type = ToolTypeEnum::from($array['type']); + static::validateFromArrayData($array, ['type']); - if ($type->isFunctionDeclarations()) { - if (!isset($array['functionDeclarations'])) { - throw new \InvalidArgumentException('Function declarations tool requires functionDeclarations field.'); - } - $declarationsData = $array['functionDeclarations']; + // Check which properties are set to determine how to construct the Tool + if (isset($array['functionDeclarations'])) { $declarations = array_map(function (array $declarationData) { return FunctionDeclaration::fromArray($declarationData); - }, $declarationsData); + }, $array['functionDeclarations']); return new self($declarations); + } elseif (isset($array['webSearch'])) { + return new self(WebSearch::fromArray($array['webSearch'])); } else { - // Web search is the only remaining option - if (!isset($array['webSearch'])) { - throw new \InvalidArgumentException('Web search tool requires webSearch field.'); - } - $webSearchData = $array['webSearch']; - return new self(WebSearch::fromArray($webSearchData)); + throw new InvalidArgumentException( + 'Tool requires either functionDeclarations or webSearch.' + ); } } } From 87d9c67880ecf0e8d652465d0eb52c4b05a61501 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 12:47:36 -0600 Subject: [PATCH 25/36] fix: corrects linting issue --- src/Messages/DTO/MessagePart.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index 98daccf3..0c880c34 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -239,7 +239,10 @@ public function toArray(): array } elseif ($this->functionResponse !== null) { $data['functionResponse'] = $this->functionResponse->toArray(); } else { - throw new RuntimeException('MessagePart requires one of: text, file, functionCall, or functionResponse. This should not be a possible condition.'); + throw new RuntimeException( + 'MessagePart requires one of: text, file, functionCall, or functionResponse. ' + . 'This should not be a possible condition.' + ); } return $data; From 30ee59c5aa4205c089255be164365cd990c6a8f4 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 15:48:59 -0600 Subject: [PATCH 26/36] refactor: adds schema constants --- src/Files/DTO/File.php | 40 ++++++----- src/Messages/DTO/Message.php | 18 ++--- src/Messages/DTO/MessagePart.php | 57 ++++++++------- src/Operations/DTO/GenerativeAiOperation.php | 33 +++++---- src/Results/DTO/Candidate.php | 25 ++++--- src/Results/DTO/GenerativeAiResult.php | 35 +++++---- src/Results/DTO/TokenUsage.php | 29 +++++--- src/Tools/DTO/FunctionCall.php | 27 +++---- src/Tools/DTO/FunctionDeclaration.php | 25 ++++--- src/Tools/DTO/FunctionResponse.php | 25 ++++--- src/Tools/DTO/Tool.php | 31 ++++---- src/Tools/DTO/WebSearch.php | 14 ++-- tests/unit/Files/DTO/FileTest.php | 44 ++++++------ tests/unit/Messages/DTO/MessagePartTest.php | 60 ++++++++-------- tests/unit/Messages/DTO/MessageTest.php | 26 +++---- .../DTO/GenerativeAiOperationTest.php | 71 ++++++++++--------- tests/unit/Results/DTO/CandidateTest.php | 34 ++++----- .../Results/DTO/GenerativeAiResultTest.php | 66 ++++++++--------- tests/unit/Results/DTO/TokenUsageTest.php | 38 +++++----- tests/unit/Tools/DTO/FunctionCallTest.php | 50 ++++++------- .../Tools/DTO/FunctionDeclarationTest.php | 48 ++++++------- tests/unit/Tools/DTO/FunctionResponseTest.php | 32 ++++----- tests/unit/Tools/DTO/ToolTest.php | 60 ++++++++-------- tests/unit/Tools/DTO/WebSearchTest.php | 34 ++++----- 24 files changed, 484 insertions(+), 438 deletions(-) diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index e71102d3..2e81f95c 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -29,6 +29,10 @@ */ final class File extends AbstractDataValueObject { + public const KEY_FILE_TYPE = 'fileType'; + public const KEY_MIME_TYPE = 'mimeType'; + public const KEY_URL = 'url'; + public const KEY_BASE64_DATA = 'base64Data'; /** * @var MimeType The MIME type of the file. */ @@ -346,44 +350,44 @@ public static function getJsonSchema(): array 'oneOf' => [ [ 'properties' => [ - 'fileType' => [ + self::KEY_FILE_TYPE => [ 'type' => 'string', 'const' => FileTypeEnum::REMOTE, 'description' => 'The file type.', ], - 'mimeType' => [ + self::KEY_MIME_TYPE => [ 'type' => 'string', 'description' => 'The MIME type of the file.', 'pattern' => '^[a-zA-Z0-9][a-zA-Z0-9!#$&\\-\\^_+.]*\\/[a-zA-Z0-9]' . '[a-zA-Z0-9!#$&\\-\\^_+.]*$', ], - 'url' => [ + self::KEY_URL => [ 'type' => 'string', 'format' => 'uri', 'description' => 'The URL to the remote file.', ], ], - 'required' => ['fileType', 'mimeType', 'url'], + 'required' => [self::KEY_FILE_TYPE, self::KEY_MIME_TYPE, self::KEY_URL], ], [ 'properties' => [ - 'fileType' => [ + self::KEY_FILE_TYPE => [ 'type' => 'string', 'const' => FileTypeEnum::INLINE, 'description' => 'The file type.', ], - 'mimeType' => [ + self::KEY_MIME_TYPE => [ 'type' => 'string', 'description' => 'The MIME type of the file.', 'pattern' => '^[a-zA-Z0-9][a-zA-Z0-9!#$&\\-\\^_+.]*\\/[a-zA-Z0-9]' . '[a-zA-Z0-9!#$&\\-\\^_+.]*$', ], - 'base64Data' => [ + self::KEY_BASE64_DATA => [ 'type' => 'string', 'description' => 'The base64-encoded file data.', ], ], - 'required' => ['fileType', 'mimeType', 'base64Data'], + 'required' => [self::KEY_FILE_TYPE, self::KEY_MIME_TYPE, self::KEY_BASE64_DATA], ], ], ]; @@ -399,14 +403,14 @@ public static function getJsonSchema(): array public function toArray(): array { $data = [ - 'fileType' => $this->fileType->value, - 'mimeType' => $this->getMimeType(), + self::KEY_FILE_TYPE => $this->fileType->value, + self::KEY_MIME_TYPE => $this->getMimeType(), ]; if ($this->url !== null) { - $data['url'] = $this->url; + $data[self::KEY_URL] = $this->url; } elseif (!$this->fileType->isRemote() && $this->base64Data !== null) { - $data['base64Data'] = $this->base64Data; + $data[self::KEY_BASE64_DATA] = $this->base64Data; } else { throw new RuntimeException( 'File requires either url or base64Data. This should not be a possible condition.' @@ -423,15 +427,15 @@ public function toArray(): array */ public static function fromArray(array $array): File { - static::validateFromArrayData($array, ['fileType']); + static::validateFromArrayData($array, [self::KEY_FILE_TYPE]); // Check which properties are set to determine how to construct the File - $mimeType = $array['mimeType'] ?? null; + $mimeType = $array[self::KEY_MIME_TYPE] ?? null; - if (isset($array['url'])) { - return new self($array['url'], $mimeType); - } elseif (isset($array['base64Data'])) { - return new self($array['base64Data'], $mimeType); + if (isset($array[self::KEY_URL])) { + return new self($array[self::KEY_URL], $mimeType); + } elseif (isset($array[self::KEY_BASE64_DATA])) { + return new self($array[self::KEY_BASE64_DATA], $mimeType); } else { throw new InvalidArgumentException('File requires either url or base64Data.'); } diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index c0983d7d..445751b8 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -27,6 +27,8 @@ */ class Message extends AbstractDataValueObject { + public const KEY_ROLE = 'role'; + public const KEY_PARTS = 'parts'; /** * @var MessageRoleEnum The role of the message sender. */ @@ -85,19 +87,19 @@ public static function getJsonSchema(): array return [ 'type' => 'object', 'properties' => [ - 'role' => [ + self::KEY_ROLE => [ 'type' => 'string', 'enum' => MessageRoleEnum::getValues(), 'description' => 'The role of the message sender.', ], - 'parts' => [ + self::KEY_PARTS => [ 'type' => 'array', 'items' => MessagePart::getJsonSchema(), 'minItems' => 1, 'description' => 'The parts that make up this message.', ], ], - 'required' => ['role', 'parts'], + 'required' => [self::KEY_ROLE, self::KEY_PARTS], ]; } @@ -111,8 +113,8 @@ public static function getJsonSchema(): array public function toArray(): array { return [ - 'role' => $this->role->value, - 'parts' => array_map(function (MessagePart $part) { + self::KEY_ROLE => $this->role->value, + self::KEY_PARTS => array_map(function (MessagePart $part) { return $part->toArray(); }, $this->parts), ]; @@ -127,10 +129,10 @@ public function toArray(): array */ final public static function fromArray(array $array): Message { - static::validateFromArrayData($array, ['role', 'parts']); + static::validateFromArrayData($array, [self::KEY_ROLE, self::KEY_PARTS]); - $role = MessageRoleEnum::from($array['role']); - $partsData = $array['parts']; + $role = MessageRoleEnum::from($array[self::KEY_ROLE]); + $partsData = $array[self::KEY_PARTS]; $parts = array_map(function (array $partData) { return MessagePart::fromArray($partData); }, $partsData); diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index 0c880c34..edc9b37d 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -36,6 +36,11 @@ */ final class MessagePart extends AbstractDataValueObject { + public const KEY_TYPE = 'type'; + public const KEY_TEXT = 'text'; + public const KEY_FILE = 'file'; + public const KEY_FUNCTION_CALL = 'functionCall'; + public const KEY_FUNCTION_RESPONSE = 'functionResponse'; /** * @var MessagePartTypeEnum The type of this message part. */ @@ -167,52 +172,52 @@ public static function getJsonSchema(): array [ 'type' => 'object', 'properties' => [ - 'type' => [ + self::KEY_TYPE => [ 'type' => 'string', 'const' => MessagePartTypeEnum::text()->value, ], - 'text' => [ + self::KEY_TEXT => [ 'type' => 'string', 'description' => 'Text content.', ], ], - 'required' => ['type', 'text'], + 'required' => [self::KEY_TYPE, self::KEY_TEXT], 'additionalProperties' => false, ], [ 'type' => 'object', 'properties' => [ - 'type' => [ + self::KEY_TYPE => [ 'type' => 'string', 'const' => MessagePartTypeEnum::file()->value, ], - 'file' => File::getJsonSchema(), + self::KEY_FILE => File::getJsonSchema(), ], - 'required' => ['type', 'file'], + 'required' => [self::KEY_TYPE, self::KEY_FILE], 'additionalProperties' => false, ], [ 'type' => 'object', 'properties' => [ - 'type' => [ + self::KEY_TYPE => [ 'type' => 'string', 'const' => MessagePartTypeEnum::functionCall()->value, ], - 'functionCall' => FunctionCall::getJsonSchema(), + self::KEY_FUNCTION_CALL => FunctionCall::getJsonSchema(), ], - 'required' => ['type', 'functionCall'], + 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_CALL], 'additionalProperties' => false, ], [ 'type' => 'object', 'properties' => [ - 'type' => [ + self::KEY_TYPE => [ 'type' => 'string', 'const' => MessagePartTypeEnum::functionResponse()->value, ], - 'functionResponse' => FunctionResponse::getJsonSchema(), + self::KEY_FUNCTION_RESPONSE => FunctionResponse::getJsonSchema(), ], - 'required' => ['type', 'functionResponse'], + 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_RESPONSE], 'additionalProperties' => false, ], ], @@ -228,16 +233,16 @@ public static function getJsonSchema(): array */ public function toArray(): array { - $data = ['type' => $this->type->value]; + $data = [self::KEY_TYPE => $this->type->value]; if ($this->text !== null) { - $data['text'] = $this->text; + $data[self::KEY_TEXT] = $this->text; } elseif ($this->file !== null) { - $data['file'] = $this->file->toArray(); + $data[self::KEY_FILE] = $this->file->toArray(); } elseif ($this->functionCall !== null) { - $data['functionCall'] = $this->functionCall->toArray(); + $data[self::KEY_FUNCTION_CALL] = $this->functionCall->toArray(); } elseif ($this->functionResponse !== null) { - $data['functionResponse'] = $this->functionResponse->toArray(); + $data[self::KEY_FUNCTION_RESPONSE] = $this->functionResponse->toArray(); } else { throw new RuntimeException( 'MessagePart requires one of: text, file, functionCall, or functionResponse. ' @@ -255,17 +260,17 @@ public function toArray(): array */ public static function fromArray(array $array): MessagePart { - static::validateFromArrayData($array, ['type']); + static::validateFromArrayData($array, [self::KEY_TYPE]); // Check which properties are set to determine how to construct the MessagePart - if (isset($array['text'])) { - return new self($array['text']); - } elseif (isset($array['file'])) { - return new self(File::fromArray($array['file'])); - } elseif (isset($array['functionCall'])) { - return new self(FunctionCall::fromArray($array['functionCall'])); - } elseif (isset($array['functionResponse'])) { - return new self(FunctionResponse::fromArray($array['functionResponse'])); + if (isset($array[self::KEY_TEXT])) { + return new self($array[self::KEY_TEXT]); + } elseif (isset($array[self::KEY_FILE])) { + return new self(File::fromArray($array[self::KEY_FILE])); + } elseif (isset($array[self::KEY_FUNCTION_CALL])) { + return new self(FunctionCall::fromArray($array[self::KEY_FUNCTION_CALL])); + } elseif (isset($array[self::KEY_FUNCTION_RESPONSE])) { + return new self(FunctionResponse::fromArray($array[self::KEY_FUNCTION_RESPONSE])); } else { throw new InvalidArgumentException( 'MessagePart requires one of: text, file, functionCall, or functionResponse.' diff --git a/src/Operations/DTO/GenerativeAiOperation.php b/src/Operations/DTO/GenerativeAiOperation.php index d0df88ec..94d10419 100644 --- a/src/Operations/DTO/GenerativeAiOperation.php +++ b/src/Operations/DTO/GenerativeAiOperation.php @@ -26,6 +26,9 @@ */ final class GenerativeAiOperation extends AbstractDataValueObject implements OperationInterface { + public const KEY_ID = 'id'; + public const KEY_STATE = 'state'; + public const KEY_RESULT = 'result'; /** * @var string Unique identifier for this operation. */ @@ -102,28 +105,28 @@ public static function getJsonSchema(): array [ 'type' => 'object', 'properties' => [ - 'id' => [ + self::KEY_ID => [ 'type' => 'string', 'description' => 'Unique identifier for this operation.', ], - 'state' => [ + self::KEY_STATE => [ 'type' => 'string', 'const' => OperationStateEnum::succeeded()->value, ], - 'result' => GenerativeAiResult::getJsonSchema(), + self::KEY_RESULT => GenerativeAiResult::getJsonSchema(), ], - 'required' => ['id', 'state', 'result'], + 'required' => [self::KEY_ID, self::KEY_STATE, self::KEY_RESULT], 'additionalProperties' => false, ], // All other states - no result [ 'type' => 'object', 'properties' => [ - 'id' => [ + self::KEY_ID => [ 'type' => 'string', 'description' => 'Unique identifier for this operation.', ], - 'state' => [ + self::KEY_STATE => [ 'type' => 'string', 'enum' => [ OperationStateEnum::starting()->value, @@ -134,7 +137,7 @@ public static function getJsonSchema(): array 'description' => 'The current state of the operation.', ], ], - 'required' => ['id', 'state'], + 'required' => [self::KEY_ID, self::KEY_STATE], 'additionalProperties' => false, ], ], @@ -151,12 +154,12 @@ public static function getJsonSchema(): array public function toArray(): array { $data = [ - 'id' => $this->id, - 'state' => $this->state->value, + self::KEY_ID => $this->id, + self::KEY_STATE => $this->state->value, ]; if ($this->result !== null) { - $data['result'] = $this->result->toArray(); + $data[self::KEY_RESULT] = $this->result->toArray(); } return $data; @@ -169,14 +172,14 @@ public function toArray(): array */ public static function fromArray(array $array): GenerativeAiOperation { - static::validateFromArrayData($array, ['id', 'state']); + static::validateFromArrayData($array, [self::KEY_ID, self::KEY_STATE]); - $state = OperationStateEnum::from($array['state']); + $state = OperationStateEnum::from($array[self::KEY_STATE]); $result = null; - if (isset($array['result'])) { - $result = GenerativeAiResult::fromArray($array['result']); + if (isset($array[self::KEY_RESULT])) { + $result = GenerativeAiResult::fromArray($array[self::KEY_RESULT]); } - return new self($array['id'], $state, $result); + return new self($array[self::KEY_ID], $state, $result); } } diff --git a/src/Results/DTO/Candidate.php b/src/Results/DTO/Candidate.php index 20ab4211..04dfffd2 100644 --- a/src/Results/DTO/Candidate.php +++ b/src/Results/DTO/Candidate.php @@ -24,6 +24,9 @@ */ final class Candidate extends AbstractDataValueObject { + public const KEY_MESSAGE = 'message'; + public const KEY_FINISH_REASON = 'finishReason'; + public const KEY_TOKEN_COUNT = 'tokenCount'; /** * @var Message The generated message. */ @@ -107,18 +110,18 @@ public static function getJsonSchema(): array return [ 'type' => 'object', 'properties' => [ - 'message' => Message::getJsonSchema(), - 'finishReason' => [ + self::KEY_MESSAGE => Message::getJsonSchema(), + self::KEY_FINISH_REASON => [ 'type' => 'string', 'enum' => FinishReasonEnum::getValues(), 'description' => 'The reason generation stopped.', ], - 'tokenCount' => [ + self::KEY_TOKEN_COUNT => [ 'type' => 'integer', 'description' => 'The number of tokens in this candidate.', ], ], - 'required' => ['message', 'finishReason', 'tokenCount'], + 'required' => [self::KEY_MESSAGE, self::KEY_FINISH_REASON, self::KEY_TOKEN_COUNT], ]; } @@ -132,9 +135,9 @@ public static function getJsonSchema(): array public function toArray(): array { return [ - 'message' => $this->message->toArray(), - 'finishReason' => $this->finishReason->value, - 'tokenCount' => $this->tokenCount, + self::KEY_MESSAGE => $this->message->toArray(), + self::KEY_FINISH_REASON => $this->finishReason->value, + self::KEY_TOKEN_COUNT => $this->tokenCount, ]; } @@ -145,14 +148,14 @@ public function toArray(): array */ public static function fromArray(array $array): Candidate { - static::validateFromArrayData($array, ['message', 'finishReason', 'tokenCount']); + static::validateFromArrayData($array, [self::KEY_MESSAGE, self::KEY_FINISH_REASON, self::KEY_TOKEN_COUNT]); - $messageData = $array['message']; + $messageData = $array[self::KEY_MESSAGE]; return new self( Message::fromArray($messageData), - FinishReasonEnum::from($array['finishReason']), - $array['tokenCount'] + FinishReasonEnum::from($array[self::KEY_FINISH_REASON]), + $array[self::KEY_TOKEN_COUNT] ); } } diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index f211fedf..9125ff9a 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -31,6 +31,10 @@ */ final class GenerativeAiResult extends AbstractDataValueObject implements ResultInterface { + public const KEY_ID = 'id'; + public const KEY_CANDIDATES = 'candidates'; + public const KEY_TOKEN_USAGE = 'tokenUsage'; + public const KEY_PROVIDER_METADATA = 'providerMetadata'; /** * @var string Unique identifier for this result. */ @@ -370,24 +374,24 @@ public static function getJsonSchema(): array return [ 'type' => 'object', 'properties' => [ - 'id' => [ + self::KEY_ID => [ 'type' => 'string', 'description' => 'Unique identifier for this result.', ], - 'candidates' => [ + self::KEY_CANDIDATES => [ 'type' => 'array', 'items' => Candidate::getJsonSchema(), 'minItems' => 1, 'description' => 'The generated candidates.', ], - 'tokenUsage' => TokenUsage::getJsonSchema(), - 'providerMetadata' => [ + self::KEY_TOKEN_USAGE => TokenUsage::getJsonSchema(), + self::KEY_PROVIDER_METADATA => [ 'type' => 'object', 'additionalProperties' => true, 'description' => 'Provider-specific metadata.', ], ], - 'required' => ['id', 'candidates', 'tokenUsage'], + 'required' => [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE], ]; } @@ -401,10 +405,10 @@ public static function getJsonSchema(): array public function toArray(): array { return [ - 'id' => $this->id, - 'candidates' => array_map(fn(Candidate $candidate) => $candidate->toArray(), $this->candidates), - 'tokenUsage' => $this->tokenUsage->toArray(), - 'providerMetadata' => $this->providerMetadata, + self::KEY_ID => $this->id, + self::KEY_CANDIDATES => array_map(fn(Candidate $candidate) => $candidate->toArray(), $this->candidates), + self::KEY_TOKEN_USAGE => $this->tokenUsage->toArray(), + self::KEY_PROVIDER_METADATA => $this->providerMetadata, ]; } @@ -415,15 +419,18 @@ public function toArray(): array */ public static function fromArray(array $array): GenerativeAiResult { - static::validateFromArrayData($array, ['id', 'candidates', 'tokenUsage']); + static::validateFromArrayData($array, [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE]); - $candidates = array_map(fn(array $candidateData) => Candidate::fromArray($candidateData), $array['candidates']); + $candidates = array_map( + fn(array $candidateData) => Candidate::fromArray($candidateData), + $array[self::KEY_CANDIDATES] + ); return new self( - $array['id'], + $array[self::KEY_ID], $candidates, - TokenUsage::fromArray($array['tokenUsage']), - $array['providerMetadata'] ?? [] + TokenUsage::fromArray($array[self::KEY_TOKEN_USAGE]), + $array[self::KEY_PROVIDER_METADATA] ?? [] ); } } diff --git a/src/Results/DTO/TokenUsage.php b/src/Results/DTO/TokenUsage.php index 5e37a5f8..cbd3774d 100644 --- a/src/Results/DTO/TokenUsage.php +++ b/src/Results/DTO/TokenUsage.php @@ -24,6 +24,9 @@ */ final class TokenUsage extends AbstractDataValueObject { + public const KEY_PROMPT_TOKENS = 'promptTokens'; + public const KEY_COMPLETION_TOKENS = 'completionTokens'; + public const KEY_TOTAL_TOKENS = 'totalTokens'; /** * @var int Number of tokens in the prompt. */ @@ -101,20 +104,20 @@ public static function getJsonSchema(): array return [ 'type' => 'object', 'properties' => [ - 'promptTokens' => [ + self::KEY_PROMPT_TOKENS => [ 'type' => 'integer', 'description' => 'Number of tokens in the prompt.', ], - 'completionTokens' => [ + self::KEY_COMPLETION_TOKENS => [ 'type' => 'integer', 'description' => 'Number of tokens in the completion.', ], - 'totalTokens' => [ + self::KEY_TOTAL_TOKENS => [ 'type' => 'integer', 'description' => 'Total number of tokens used.', ], ], - 'required' => ['promptTokens', 'completionTokens', 'totalTokens'], + 'required' => [self::KEY_PROMPT_TOKENS, self::KEY_COMPLETION_TOKENS, self::KEY_TOTAL_TOKENS], ]; } @@ -128,9 +131,9 @@ public static function getJsonSchema(): array public function toArray(): array { return [ - 'promptTokens' => $this->promptTokens, - 'completionTokens' => $this->completionTokens, - 'totalTokens' => $this->totalTokens, + self::KEY_PROMPT_TOKENS => $this->promptTokens, + self::KEY_COMPLETION_TOKENS => $this->completionTokens, + self::KEY_TOTAL_TOKENS => $this->totalTokens, ]; } @@ -141,12 +144,16 @@ public function toArray(): array */ public static function fromArray(array $array): TokenUsage { - static::validateFromArrayData($array, ['promptTokens', 'completionTokens', 'totalTokens']); + static::validateFromArrayData($array, [ + self::KEY_PROMPT_TOKENS, + self::KEY_COMPLETION_TOKENS, + self::KEY_TOTAL_TOKENS + ]); return new self( - $array['promptTokens'], - $array['completionTokens'], - $array['totalTokens'] + $array[self::KEY_PROMPT_TOKENS], + $array[self::KEY_COMPLETION_TOKENS], + $array[self::KEY_TOTAL_TOKENS] ); } } diff --git a/src/Tools/DTO/FunctionCall.php b/src/Tools/DTO/FunctionCall.php index 59ceca6f..b5adda57 100644 --- a/src/Tools/DTO/FunctionCall.php +++ b/src/Tools/DTO/FunctionCall.php @@ -20,6 +20,9 @@ */ final class FunctionCall extends AbstractDataValueObject { + public const KEY_ID = 'id'; + public const KEY_NAME = 'name'; + public const KEY_ARGS = 'args'; /** * @var string|null Unique identifier for this function call. */ @@ -102,15 +105,15 @@ public static function getJsonSchema(): array return [ 'type' => 'object', 'properties' => [ - 'id' => [ + self::KEY_ID => [ 'type' => 'string', 'description' => 'Unique identifier for this function call.', ], - 'name' => [ + self::KEY_NAME => [ 'type' => 'string', 'description' => 'The name of the function to call.', ], - 'args' => [ + self::KEY_ARGS => [ 'type' => 'object', 'description' => 'The arguments to pass to the function.', 'additionalProperties' => true, @@ -118,13 +121,13 @@ public static function getJsonSchema(): array ], 'oneOf' => [ [ - 'required' => ['id'], + 'required' => [self::KEY_ID], ], [ - 'required' => ['name'], + 'required' => [self::KEY_NAME], ], [ - 'required' => ['id', 'name'], + 'required' => [self::KEY_ID, self::KEY_NAME], ], ], ]; @@ -142,15 +145,15 @@ public function toArray(): array $data = []; if ($this->id !== null) { - $data['id'] = $this->id; + $data[self::KEY_ID] = $this->id; } if ($this->name !== null) { - $data['name'] = $this->name; + $data[self::KEY_NAME] = $this->name; } if (!empty($this->args)) { - $data['args'] = $this->args; + $data[self::KEY_ARGS] = $this->args; } return $data; @@ -164,9 +167,9 @@ public function toArray(): array public static function fromArray(array $array): FunctionCall { return new self( - $array['id'] ?? null, - $array['name'] ?? null, - $array['args'] ?? [] + $array[self::KEY_ID] ?? null, + $array[self::KEY_NAME] ?? null, + $array[self::KEY_ARGS] ?? [] ); } } diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index 1374bd0c..293d4e5a 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -21,6 +21,9 @@ */ final class FunctionDeclaration extends AbstractDataValueObject { + public const KEY_NAME = 'name'; + public const KEY_DESCRIPTION = 'description'; + public const KEY_PARAMETERS = 'parameters'; /** * @var string The name of the function. */ @@ -98,20 +101,20 @@ public static function getJsonSchema(): array return [ 'type' => 'object', 'properties' => [ - 'name' => [ + self::KEY_NAME => [ 'type' => 'string', 'description' => 'The name of the function.', ], - 'description' => [ + self::KEY_DESCRIPTION => [ 'type' => 'string', 'description' => 'A description of what the function does.', ], - 'parameters' => [ + self::KEY_PARAMETERS => [ 'type' => ['string', 'number', 'boolean', 'object', 'array', 'null'], 'description' => 'The JSON schema for the function parameters.', ], ], - 'required' => ['name', 'description'], + 'required' => [self::KEY_NAME, self::KEY_DESCRIPTION], ]; } @@ -125,12 +128,12 @@ public static function getJsonSchema(): array public function toArray(): array { $data = [ - 'name' => $this->name, - 'description' => $this->description, + self::KEY_NAME => $this->name, + self::KEY_DESCRIPTION => $this->description, ]; if ($this->parameters !== null) { - $data['parameters'] = $this->parameters; + $data[self::KEY_PARAMETERS] = $this->parameters; } return $data; @@ -143,12 +146,12 @@ public function toArray(): array */ public static function fromArray(array $array): FunctionDeclaration { - static::validateFromArrayData($array, ['name', 'description']); + static::validateFromArrayData($array, [self::KEY_NAME, self::KEY_DESCRIPTION]); return new self( - $array['name'], - $array['description'], - $array['parameters'] ?? null + $array[self::KEY_NAME], + $array[self::KEY_DESCRIPTION], + $array[self::KEY_PARAMETERS] ?? null ); } } diff --git a/src/Tools/DTO/FunctionResponse.php b/src/Tools/DTO/FunctionResponse.php index 9f368b17..ba4bf0f6 100644 --- a/src/Tools/DTO/FunctionResponse.php +++ b/src/Tools/DTO/FunctionResponse.php @@ -20,6 +20,9 @@ */ final class FunctionResponse extends AbstractDataValueObject { + public const KEY_ID = 'id'; + public const KEY_NAME = 'name'; + public const KEY_RESPONSE = 'response'; /** * @var string The ID of the function call this is responding to. */ @@ -97,20 +100,20 @@ public static function getJsonSchema(): array return [ 'type' => 'object', 'properties' => [ - 'id' => [ + self::KEY_ID => [ 'type' => 'string', 'description' => 'The ID of the function call this is responding to.', ], - 'name' => [ + self::KEY_NAME => [ 'type' => 'string', 'description' => 'The name of the function that was called.', ], - 'response' => [ + self::KEY_RESPONSE => [ 'type' => ['string', 'number', 'boolean', 'object', 'array', 'null'], 'description' => 'The response data from the function.', ], ], - 'required' => ['id', 'name', 'response'], + 'required' => [self::KEY_ID, self::KEY_NAME, self::KEY_RESPONSE], ]; } @@ -124,9 +127,9 @@ public static function getJsonSchema(): array public function toArray(): array { return [ - 'id' => $this->id, - 'name' => $this->name, - 'response' => $this->response, + self::KEY_ID => $this->id, + self::KEY_NAME => $this->name, + self::KEY_RESPONSE => $this->response, ]; } @@ -137,12 +140,12 @@ public function toArray(): array */ public static function fromArray(array $array): FunctionResponse { - static::validateFromArrayData($array, ['id', 'name', 'response']); + static::validateFromArrayData($array, [self::KEY_ID, self::KEY_NAME, self::KEY_RESPONSE]); return new self( - $array['id'], - $array['name'], - $array['response'] + $array[self::KEY_ID], + $array[self::KEY_NAME], + $array[self::KEY_RESPONSE] ); } } diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index a6d3f2a7..9bb69bce 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -29,6 +29,9 @@ */ final class Tool extends AbstractDataValueObject { + public const KEY_TYPE = 'type'; + public const KEY_FUNCTION_DECLARATIONS = 'functionDeclarations'; + public const KEY_WEB_SEARCH = 'webSearch'; /** * @var ToolTypeEnum The type of tool. */ @@ -115,30 +118,30 @@ public static function getJsonSchema(): array [ 'type' => 'object', 'properties' => [ - 'type' => [ + self::KEY_TYPE => [ 'type' => 'string', 'const' => ToolTypeEnum::functionDeclarations()->value, 'description' => 'The type of tool.', ], - 'functionDeclarations' => [ + self::KEY_FUNCTION_DECLARATIONS => [ 'type' => 'array', 'items' => FunctionDeclaration::getJsonSchema(), 'description' => 'Function declarations.', ], ], - 'required' => ['type', 'functionDeclarations'], + 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_DECLARATIONS], ], [ 'type' => 'object', 'properties' => [ - 'type' => [ + self::KEY_TYPE => [ 'type' => 'string', 'const' => ToolTypeEnum::webSearch()->value, 'description' => 'The type of tool.', ], - 'webSearch' => WebSearch::getJsonSchema(), + self::KEY_WEB_SEARCH => WebSearch::getJsonSchema(), ], - 'required' => ['type', 'webSearch'], + 'required' => [self::KEY_TYPE, self::KEY_WEB_SEARCH], ], ], ]; @@ -153,14 +156,14 @@ public static function getJsonSchema(): array */ public function toArray(): array { - $data = ['type' => $this->type->value]; + $data = [self::KEY_TYPE => $this->type->value]; if ($this->type->isFunctionDeclarations() && $this->functionDeclarations !== null) { - $data['functionDeclarations'] = array_map(function (FunctionDeclaration $declaration) { + $data[self::KEY_FUNCTION_DECLARATIONS] = array_map(function (FunctionDeclaration $declaration) { return $declaration->toArray(); }, $this->functionDeclarations); } elseif ($this->type->isWebSearch() && $this->webSearch !== null) { - $data['webSearch'] = $this->webSearch->toArray(); + $data[self::KEY_WEB_SEARCH] = $this->webSearch->toArray(); } return $data; @@ -173,16 +176,16 @@ public function toArray(): array */ public static function fromArray(array $array): Tool { - static::validateFromArrayData($array, ['type']); + static::validateFromArrayData($array, [self::KEY_TYPE]); // Check which properties are set to determine how to construct the Tool - if (isset($array['functionDeclarations'])) { + if (isset($array[self::KEY_FUNCTION_DECLARATIONS])) { $declarations = array_map(function (array $declarationData) { return FunctionDeclaration::fromArray($declarationData); - }, $array['functionDeclarations']); + }, $array[self::KEY_FUNCTION_DECLARATIONS]); return new self($declarations); - } elseif (isset($array['webSearch'])) { - return new self(WebSearch::fromArray($array['webSearch'])); + } elseif (isset($array[self::KEY_WEB_SEARCH])) { + return new self(WebSearch::fromArray($array[self::KEY_WEB_SEARCH])); } else { throw new InvalidArgumentException( 'Tool requires either functionDeclarations or webSearch.' diff --git a/src/Tools/DTO/WebSearch.php b/src/Tools/DTO/WebSearch.php index 373ff36d..52fcdffa 100644 --- a/src/Tools/DTO/WebSearch.php +++ b/src/Tools/DTO/WebSearch.php @@ -20,6 +20,8 @@ */ final class WebSearch extends AbstractDataValueObject { + public const KEY_ALLOWED_DOMAINS = 'allowedDomains'; + public const KEY_DISALLOWED_DOMAINS = 'disallowedDomains'; /** * @var string[] List of domains that are allowed for web search. */ @@ -78,14 +80,14 @@ public static function getJsonSchema(): array return [ 'type' => 'object', 'properties' => [ - 'allowedDomains' => [ + self::KEY_ALLOWED_DOMAINS => [ 'type' => 'array', 'items' => [ 'type' => 'string', ], 'description' => 'List of domains that are allowed for web search.', ], - 'disallowedDomains' => [ + self::KEY_DISALLOWED_DOMAINS => [ 'type' => 'array', 'items' => [ 'type' => 'string', @@ -107,8 +109,8 @@ public static function getJsonSchema(): array public function toArray(): array { return [ - 'allowedDomains' => $this->allowedDomains, - 'disallowedDomains' => $this->disallowedDomains, + self::KEY_ALLOWED_DOMAINS => $this->allowedDomains, + self::KEY_DISALLOWED_DOMAINS => $this->disallowedDomains, ]; } @@ -120,8 +122,8 @@ public function toArray(): array public static function fromArray(array $array): WebSearch { return new self( - $array['allowedDomains'] ?? [], - $array['disallowedDomains'] ?? [] + $array[self::KEY_ALLOWED_DOMAINS] ?? [], + $array[self::KEY_DISALLOWED_DOMAINS] ?? [] ); } } diff --git a/tests/unit/Files/DTO/FileTest.php b/tests/unit/Files/DTO/FileTest.php index 73089a6d..931ca16b 100644 --- a/tests/unit/Files/DTO/FileTest.php +++ b/tests/unit/Files/DTO/FileTest.php @@ -225,18 +225,18 @@ public function testJsonSchema(): void // Check remote file schema $remoteSchema = $schema['oneOf'][0]; $this->assertArrayHasKey('properties', $remoteSchema); - $this->assertArrayHasKey('fileType', $remoteSchema['properties']); - $this->assertArrayHasKey('mimeType', $remoteSchema['properties']); - $this->assertArrayHasKey('url', $remoteSchema['properties']); - $this->assertEquals(['fileType', 'mimeType', 'url'], $remoteSchema['required']); + $this->assertArrayHasKey(File::KEY_FILE_TYPE, $remoteSchema['properties']); + $this->assertArrayHasKey(File::KEY_MIME_TYPE, $remoteSchema['properties']); + $this->assertArrayHasKey(File::KEY_URL, $remoteSchema['properties']); + $this->assertEquals([File::KEY_FILE_TYPE, File::KEY_MIME_TYPE, File::KEY_URL], $remoteSchema['required']); // Check inline file schema $inlineSchema = $schema['oneOf'][1]; $this->assertArrayHasKey('properties', $inlineSchema); - $this->assertArrayHasKey('fileType', $inlineSchema['properties']); - $this->assertArrayHasKey('mimeType', $inlineSchema['properties']); - $this->assertArrayHasKey('base64Data', $inlineSchema['properties']); - $this->assertEquals(['fileType', 'mimeType', 'base64Data'], $inlineSchema['required']); + $this->assertArrayHasKey(File::KEY_FILE_TYPE, $inlineSchema['properties']); + $this->assertArrayHasKey(File::KEY_MIME_TYPE, $inlineSchema['properties']); + $this->assertArrayHasKey(File::KEY_BASE64_DATA, $inlineSchema['properties']); + $this->assertEquals([File::KEY_FILE_TYPE, File::KEY_MIME_TYPE, File::KEY_BASE64_DATA], $inlineSchema['required']); } /** @@ -279,10 +279,10 @@ public function testToArrayRemoteFile(): void $json = $file->toArray(); $this->assertIsArray($json); - $this->assertEquals(\WordPress\AiClient\Files\Enums\FileTypeEnum::remote()->value, $json['fileType']); - $this->assertEquals('image/jpeg', $json['mimeType']); - $this->assertEquals('https://example.com/image.jpg', $json['url']); - $this->assertArrayNotHasKey('base64Data', $json); + $this->assertEquals(\WordPress\AiClient\Files\Enums\FileTypeEnum::remote()->value, $json[File::KEY_FILE_TYPE]); + $this->assertEquals('image/jpeg', $json[File::KEY_MIME_TYPE]); + $this->assertEquals('https://example.com/image.jpg', $json[File::KEY_URL]); + $this->assertArrayNotHasKey(File::KEY_BASE64_DATA, $json); } /** @@ -298,10 +298,10 @@ public function testToArrayInlineFile(): void $json = $file->toArray(); $this->assertIsArray($json); - $this->assertEquals(\WordPress\AiClient\Files\Enums\FileTypeEnum::inline()->value, $json['fileType']); - $this->assertEquals('text/plain', $json['mimeType']); - $this->assertEquals($base64Data, $json['base64Data']); - $this->assertArrayNotHasKey('url', $json); + $this->assertEquals(\WordPress\AiClient\Files\Enums\FileTypeEnum::inline()->value, $json[File::KEY_FILE_TYPE]); + $this->assertEquals('text/plain', $json[File::KEY_MIME_TYPE]); + $this->assertEquals($base64Data, $json[File::KEY_BASE64_DATA]); + $this->assertArrayNotHasKey(File::KEY_URL, $json); } /** @@ -312,9 +312,9 @@ public function testToArrayInlineFile(): void public function testFromArrayRemoteFile(): void { $json = [ - 'fileType' => \WordPress\AiClient\Files\Enums\FileTypeEnum::remote()->value, - 'mimeType' => 'image/png', - 'url' => 'https://example.com/test.png' + File::KEY_FILE_TYPE => \WordPress\AiClient\Files\Enums\FileTypeEnum::remote()->value, + File::KEY_MIME_TYPE => 'image/png', + File::KEY_URL => 'https://example.com/test.png' ]; $file = File::fromArray($json); @@ -335,9 +335,9 @@ public function testFromArrayInlineFile(): void { $base64Data = 'SGVsbG8gV29ybGQ='; $json = [ - 'fileType' => \WordPress\AiClient\Files\Enums\FileTypeEnum::inline()->value, - 'mimeType' => 'text/plain', - 'base64Data' => $base64Data + File::KEY_FILE_TYPE => \WordPress\AiClient\Files\Enums\FileTypeEnum::inline()->value, + File::KEY_MIME_TYPE => 'text/plain', + File::KEY_BASE64_DATA => $base64Data ]; $file = File::fromArray($json); diff --git a/tests/unit/Messages/DTO/MessagePartTest.php b/tests/unit/Messages/DTO/MessagePartTest.php index 08078014..f1783c94 100644 --- a/tests/unit/Messages/DTO/MessagePartTest.php +++ b/tests/unit/Messages/DTO/MessagePartTest.php @@ -150,30 +150,30 @@ public function testJsonSchema(): void // Check text variant $textSchema = $schema['oneOf'][0]; $this->assertEquals('object', $textSchema['type']); - $this->assertEquals(MessagePartTypeEnum::text()->value, $textSchema['properties']['type']['const']); - $this->assertArrayHasKey('text', $textSchema['properties']); - $this->assertEquals(['type', 'text'], $textSchema['required']); + $this->assertEquals(MessagePartTypeEnum::text()->value, $textSchema['properties'][MessagePart::KEY_TYPE]['const']); + $this->assertArrayHasKey(MessagePart::KEY_TEXT, $textSchema['properties']); + $this->assertEquals([MessagePart::KEY_TYPE, MessagePart::KEY_TEXT], $textSchema['required']); // Check file variant $fileSchema = $schema['oneOf'][1]; $this->assertEquals('object', $fileSchema['type']); - $this->assertEquals(MessagePartTypeEnum::file()->value, $fileSchema['properties']['type']['const']); - $this->assertArrayHasKey('file', $fileSchema['properties']); - $this->assertEquals(['type', 'file'], $fileSchema['required']); + $this->assertEquals(MessagePartTypeEnum::file()->value, $fileSchema['properties'][MessagePart::KEY_TYPE]['const']); + $this->assertArrayHasKey(MessagePart::KEY_FILE, $fileSchema['properties']); + $this->assertEquals([MessagePart::KEY_TYPE, MessagePart::KEY_FILE], $fileSchema['required']); // Check function_call variant $functionCallSchema = $schema['oneOf'][2]; $this->assertEquals('object', $functionCallSchema['type']); - $this->assertEquals(MessagePartTypeEnum::functionCall()->value, $functionCallSchema['properties']['type']['const']); - $this->assertArrayHasKey('functionCall', $functionCallSchema['properties']); - $this->assertEquals(['type', 'functionCall'], $functionCallSchema['required']); + $this->assertEquals(MessagePartTypeEnum::functionCall()->value, $functionCallSchema['properties'][MessagePart::KEY_TYPE]['const']); + $this->assertArrayHasKey(MessagePart::KEY_FUNCTION_CALL, $functionCallSchema['properties']); + $this->assertEquals([MessagePart::KEY_TYPE, MessagePart::KEY_FUNCTION_CALL], $functionCallSchema['required']); // Check function_response variant $functionResponseSchema = $schema['oneOf'][3]; $this->assertEquals('object', $functionResponseSchema['type']); - $this->assertEquals(MessagePartTypeEnum::functionResponse()->value, $functionResponseSchema['properties']['type']['const']); - $this->assertArrayHasKey('functionResponse', $functionResponseSchema['properties']); - $this->assertEquals(['type', 'functionResponse'], $functionResponseSchema['required']); + $this->assertEquals(MessagePartTypeEnum::functionResponse()->value, $functionResponseSchema['properties'][MessagePart::KEY_TYPE]['const']); + $this->assertArrayHasKey(MessagePart::KEY_FUNCTION_RESPONSE, $functionResponseSchema['properties']); + $this->assertEquals([MessagePart::KEY_TYPE, MessagePart::KEY_FUNCTION_RESPONSE], $functionResponseSchema['required']); } /** @@ -243,15 +243,15 @@ public function testToArrayWithText(): void $json = $part->toArray(); $this->assertIsArray($json); - $this->assertArrayHasKey('type', $json); - $this->assertArrayHasKey('text', $json); - $this->assertEquals(MessagePartTypeEnum::text()->value, $json['type']); - $this->assertEquals('Hello, world!', $json['text']); + $this->assertArrayHasKey(MessagePart::KEY_TYPE, $json); + $this->assertArrayHasKey(MessagePart::KEY_TEXT, $json); + $this->assertEquals(MessagePartTypeEnum::text()->value, $json[MessagePart::KEY_TYPE]); + $this->assertEquals('Hello, world!', $json[MessagePart::KEY_TEXT]); // Ensure other fields are not present - $this->assertArrayNotHasKey('file', $json); - $this->assertArrayNotHasKey('functionCall', $json); - $this->assertArrayNotHasKey('functionResponse', $json); + $this->assertArrayNotHasKey(MessagePart::KEY_FILE, $json); + $this->assertArrayNotHasKey(MessagePart::KEY_FUNCTION_CALL, $json); + $this->assertArrayNotHasKey(MessagePart::KEY_FUNCTION_RESPONSE, $json); } /** @@ -266,10 +266,10 @@ public function testToArrayWithFile(): void $json = $part->toArray(); $this->assertIsArray($json); - $this->assertArrayHasKey('type', $json); - $this->assertArrayHasKey('file', $json); - $this->assertEquals(MessagePartTypeEnum::file()->value, $json['type']); - $this->assertIsArray($json['file']); + $this->assertArrayHasKey(MessagePart::KEY_TYPE, $json); + $this->assertArrayHasKey(MessagePart::KEY_FILE, $json); + $this->assertEquals(MessagePartTypeEnum::file()->value, $json[MessagePart::KEY_TYPE]); + $this->assertIsArray($json[MessagePart::KEY_FILE]); } /** @@ -280,8 +280,8 @@ public function testToArrayWithFile(): void public function testFromArrayWithText(): void { $json = [ - 'type' => MessagePartTypeEnum::text()->value, - 'text' => 'Test message' + MessagePart::KEY_TYPE => MessagePartTypeEnum::text()->value, + MessagePart::KEY_TEXT => 'Test message' ]; $part = MessagePart::fromArray($json); @@ -298,11 +298,11 @@ public function testFromArrayWithText(): void public function testFromArrayWithFile(): void { $json = [ - 'type' => MessagePartTypeEnum::file()->value, - 'file' => [ - 'fileType' => FileTypeEnum::remote()->value, - 'mimeType' => 'image/jpeg', - 'url' => 'https://example.com/image.jpg' + MessagePart::KEY_TYPE => MessagePartTypeEnum::file()->value, + MessagePart::KEY_FILE => [ + File::KEY_FILE_TYPE => FileTypeEnum::remote()->value, + File::KEY_MIME_TYPE => 'image/jpeg', + File::KEY_URL => 'https://example.com/image.jpg' ] ]; diff --git a/tests/unit/Messages/DTO/MessageTest.php b/tests/unit/Messages/DTO/MessageTest.php index 84781f26..0d28933c 100644 --- a/tests/unit/Messages/DTO/MessageTest.php +++ b/tests/unit/Messages/DTO/MessageTest.php @@ -141,11 +141,11 @@ public function testJsonSchema(): void // Check properties $this->assertArrayHasKey('properties', $schema); - $this->assertArrayHasKey('role', $schema['properties']); - $this->assertArrayHasKey('parts', $schema['properties']); + $this->assertArrayHasKey(Message::KEY_ROLE, $schema['properties']); + $this->assertArrayHasKey(Message::KEY_PARTS, $schema['properties']); // Check role property - $roleSchema = $schema['properties']['role']; + $roleSchema = $schema['properties'][Message::KEY_ROLE]; $this->assertEquals('string', $roleSchema['type']); $this->assertArrayHasKey('enum', $roleSchema); $this->assertContains('system', $roleSchema['enum']); @@ -153,14 +153,14 @@ public function testJsonSchema(): void $this->assertContains('model', $roleSchema['enum']); // Check parts property - $partsSchema = $schema['properties']['parts']; + $partsSchema = $schema['properties'][Message::KEY_PARTS]; $this->assertEquals('array', $partsSchema['type']); $this->assertArrayHasKey('items', $partsSchema); $this->assertIsArray($partsSchema['items']); // Check required fields $this->assertArrayHasKey('required', $schema); - $this->assertEquals(['role', 'parts'], $schema['required']); + $this->assertEquals([Message::KEY_ROLE, Message::KEY_PARTS], $schema['required']); } /** @@ -245,11 +245,11 @@ public function testToArray(): void $json = $message->toArray(); $this->assertIsArray($json); - $this->assertEquals($role->value, $json['role']); - $this->assertIsArray($json['parts']); - $this->assertCount(2, $json['parts']); - $this->assertEquals('Hello, world!', $json['parts'][0]['text']); - $this->assertEquals('How are you?', $json['parts'][1]['text']); + $this->assertEquals($role->value, $json[Message::KEY_ROLE]); + $this->assertIsArray($json[Message::KEY_PARTS]); + $this->assertCount(2, $json[Message::KEY_PARTS]); + $this->assertEquals('Hello, world!', $json[Message::KEY_PARTS][0][MessagePart::KEY_TEXT]); + $this->assertEquals('How are you?', $json[Message::KEY_PARTS][1][MessagePart::KEY_TEXT]); } /** @@ -260,9 +260,9 @@ public function testToArray(): void public function testFromArray(): void { $json = [ - 'role' => MessageRoleEnum::system()->value, - 'parts' => [ - ['type' => MessagePartTypeEnum::text()->value, 'text' => 'You are a helpful assistant.'] + Message::KEY_ROLE => MessageRoleEnum::system()->value, + Message::KEY_PARTS => [ + [MessagePart::KEY_TYPE => MessagePartTypeEnum::text()->value, MessagePart::KEY_TEXT => 'You are a helpful assistant.'] ] ]; diff --git a/tests/unit/Operations/DTO/GenerativeAiOperationTest.php b/tests/unit/Operations/DTO/GenerativeAiOperationTest.php index 40e4cc9a..5d929202 100644 --- a/tests/unit/Operations/DTO/GenerativeAiOperationTest.php +++ b/tests/unit/Operations/DTO/GenerativeAiOperationTest.php @@ -11,6 +11,7 @@ use WordPress\AiClient\Messages\DTO\MessagePart; use WordPress\AiClient\Messages\DTO\ModelMessage; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; +use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Results\DTO\Candidate; use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Results\DTO\TokenUsage; @@ -234,18 +235,18 @@ public function testJsonSchemaForSucceededState(): void $succeededSchema = $schema['oneOf'][0]; $this->assertEquals('object', $succeededSchema['type']); $this->assertArrayHasKey('properties', $succeededSchema); - $this->assertArrayHasKey('id', $succeededSchema['properties']); - $this->assertArrayHasKey('state', $succeededSchema['properties']); - $this->assertArrayHasKey('result', $succeededSchema['properties']); + $this->assertArrayHasKey(GenerativeAiOperation::KEY_ID, $succeededSchema['properties']); + $this->assertArrayHasKey(GenerativeAiOperation::KEY_STATE, $succeededSchema['properties']); + $this->assertArrayHasKey(GenerativeAiOperation::KEY_RESULT, $succeededSchema['properties']); // State should be const for succeeded $this->assertEquals( OperationStateEnum::succeeded()->value, - $succeededSchema['properties']['state']['const'] + $succeededSchema['properties'][GenerativeAiOperation::KEY_STATE]['const'] ); // Required fields - $this->assertEquals(['id', 'state', 'result'], $succeededSchema['required']); + $this->assertEquals([GenerativeAiOperation::KEY_ID, GenerativeAiOperation::KEY_STATE, GenerativeAiOperation::KEY_RESULT], $succeededSchema['required']); } /** @@ -261,19 +262,19 @@ public function testJsonSchemaForNonSucceededStates(): void $otherStatesSchema = $schema['oneOf'][1]; $this->assertEquals('object', $otherStatesSchema['type']); $this->assertArrayHasKey('properties', $otherStatesSchema); - $this->assertArrayHasKey('id', $otherStatesSchema['properties']); - $this->assertArrayHasKey('state', $otherStatesSchema['properties']); - $this->assertArrayNotHasKey('result', $otherStatesSchema['properties']); + $this->assertArrayHasKey(GenerativeAiOperation::KEY_ID, $otherStatesSchema['properties']); + $this->assertArrayHasKey(GenerativeAiOperation::KEY_STATE, $otherStatesSchema['properties']); + $this->assertArrayNotHasKey(GenerativeAiOperation::KEY_RESULT, $otherStatesSchema['properties']); // State should be enum for other states - $stateEnum = $otherStatesSchema['properties']['state']['enum']; + $stateEnum = $otherStatesSchema['properties'][GenerativeAiOperation::KEY_STATE]['enum']; $this->assertContains(OperationStateEnum::starting()->value, $stateEnum); $this->assertContains(OperationStateEnum::processing()->value, $stateEnum); $this->assertContains(OperationStateEnum::failed()->value, $stateEnum); $this->assertContains(OperationStateEnum::canceled()->value, $stateEnum); // Required fields - $this->assertEquals(['id', 'state'], $otherStatesSchema['required']); + $this->assertEquals([GenerativeAiOperation::KEY_ID, GenerativeAiOperation::KEY_STATE], $otherStatesSchema['required']); } /** @@ -305,10 +306,10 @@ public function testToArrayStartingState(): void $json = $this->assertToArrayReturnsArray($operation); - $this->assertArrayHasKeys($json, ['id', 'state']); - $this->assertArrayNotHasKeys($json, ['result']); - $this->assertEquals('op_start_123', $json['id']); - $this->assertEquals(OperationStateEnum::starting()->value, $json['state']); + $this->assertArrayHasKeys($json, [GenerativeAiOperation::KEY_ID, GenerativeAiOperation::KEY_STATE]); + $this->assertArrayNotHasKeys($json, [GenerativeAiOperation::KEY_RESULT]); + $this->assertEquals('op_start_123', $json[GenerativeAiOperation::KEY_ID]); + $this->assertEquals(OperationStateEnum::starting()->value, $json[GenerativeAiOperation::KEY_STATE]); } /** @@ -341,11 +342,11 @@ public function testToArraySucceededState(): void $json = $this->assertToArrayReturnsArray($operation); - $this->assertArrayHasKeys($json, ['id', 'state', 'result']); - $this->assertEquals('op_success_456', $json['id']); - $this->assertEquals(OperationStateEnum::succeeded()->value, $json['state']); - $this->assertIsArray($json['result']); - $this->assertEquals('result_success', $json['result']['id']); + $this->assertArrayHasKeys($json, [GenerativeAiOperation::KEY_ID, GenerativeAiOperation::KEY_STATE, GenerativeAiOperation::KEY_RESULT]); + $this->assertEquals('op_success_456', $json[GenerativeAiOperation::KEY_ID]); + $this->assertEquals(OperationStateEnum::succeeded()->value, $json[GenerativeAiOperation::KEY_STATE]); + $this->assertIsArray($json[GenerativeAiOperation::KEY_RESULT]); + $this->assertEquals('result_success', $json[GenerativeAiOperation::KEY_RESULT][GenerativeAiResult::KEY_ID]); } /** @@ -356,8 +357,8 @@ public function testToArraySucceededState(): void public function testFromArrayStartingState(): void { $json = [ - 'id' => 'op_from_json_start', - 'state' => OperationStateEnum::starting()->value + GenerativeAiOperation::KEY_ID => 'op_from_json_start', + GenerativeAiOperation::KEY_STATE => OperationStateEnum::starting()->value ]; $operation = GenerativeAiOperation::fromArray($json); @@ -376,24 +377,24 @@ public function testFromArrayStartingState(): void public function testFromArraySucceededState(): void { $json = [ - 'id' => 'op_from_json_success', - 'state' => OperationStateEnum::succeeded()->value, - 'result' => [ - 'id' => 'result_from_json', - 'candidates' => [ + GenerativeAiOperation::KEY_ID => 'op_from_json_success', + GenerativeAiOperation::KEY_STATE => OperationStateEnum::succeeded()->value, + GenerativeAiOperation::KEY_RESULT => [ + GenerativeAiResult::KEY_ID => 'result_from_json', + GenerativeAiResult::KEY_CANDIDATES => [ [ - 'message' => [ - 'role' => MessageRoleEnum::model()->value, - 'parts' => [['type' => 'text', 'text' => 'Response text']] + Candidate::KEY_MESSAGE => [ + Message::KEY_ROLE => MessageRoleEnum::model()->value, + Message::KEY_PARTS => [[MessagePart::KEY_TYPE => 'text', MessagePart::KEY_TEXT => 'Response text']] ], - 'finishReason' => FinishReasonEnum::stop()->value, - 'tokenCount' => 30 + Candidate::KEY_FINISH_REASON => FinishReasonEnum::stop()->value, + Candidate::KEY_TOKEN_COUNT => 30 ] ], - 'tokenUsage' => [ - 'promptTokens' => 10, - 'completionTokens' => 30, - 'totalTokens' => 40 + GenerativeAiResult::KEY_TOKEN_USAGE => [ + TokenUsage::KEY_PROMPT_TOKENS => 10, + TokenUsage::KEY_COMPLETION_TOKENS => 30, + TokenUsage::KEY_TOTAL_TOKENS => 40 ] ] ]; diff --git a/tests/unit/Results/DTO/CandidateTest.php b/tests/unit/Results/DTO/CandidateTest.php index 92b8e6b3..d9875081 100644 --- a/tests/unit/Results/DTO/CandidateTest.php +++ b/tests/unit/Results/DTO/CandidateTest.php @@ -230,12 +230,12 @@ public function testJsonSchema(): void // Check properties $this->assertArrayHasKey('properties', $schema); - $this->assertArrayHasKey('message', $schema['properties']); - $this->assertArrayHasKey('finishReason', $schema['properties']); - $this->assertArrayHasKey('tokenCount', $schema['properties']); + $this->assertArrayHasKey(Candidate::KEY_MESSAGE, $schema['properties']); + $this->assertArrayHasKey(Candidate::KEY_FINISH_REASON, $schema['properties']); + $this->assertArrayHasKey(Candidate::KEY_TOKEN_COUNT, $schema['properties']); // Check finishReason property - $finishReasonSchema = $schema['properties']['finishReason']; + $finishReasonSchema = $schema['properties'][Candidate::KEY_FINISH_REASON]; $this->assertEquals('string', $finishReasonSchema['type']); $this->assertArrayHasKey('enum', $finishReasonSchema); $this->assertContains('stop', $finishReasonSchema['enum']); @@ -245,12 +245,12 @@ public function testJsonSchema(): void $this->assertContains('error', $finishReasonSchema['enum']); // Check tokenCount property - $tokenCountSchema = $schema['properties']['tokenCount']; + $tokenCountSchema = $schema['properties'][Candidate::KEY_TOKEN_COUNT]; $this->assertEquals('integer', $tokenCountSchema['type']); // Check required fields $this->assertArrayHasKey('required', $schema); - $this->assertEquals(['message', 'finishReason', 'tokenCount'], $schema['required']); + $this->assertEquals([Candidate::KEY_MESSAGE, Candidate::KEY_FINISH_REASON, Candidate::KEY_TOKEN_COUNT], $schema['required']); } /** @@ -353,10 +353,10 @@ public function testToArray(): void $json = $this->assertToArrayReturnsArray($candidate); - $this->assertArrayHasKeys($json, ['message', 'finishReason', 'tokenCount']); - $this->assertIsArray($json['message']); - $this->assertEquals(FinishReasonEnum::stop()->value, $json['finishReason']); - $this->assertEquals(45, $json['tokenCount']); + $this->assertArrayHasKeys($json, [Candidate::KEY_MESSAGE, Candidate::KEY_FINISH_REASON, Candidate::KEY_TOKEN_COUNT]); + $this->assertIsArray($json[Candidate::KEY_MESSAGE]); + $this->assertEquals(FinishReasonEnum::stop()->value, $json[Candidate::KEY_FINISH_REASON]); + $this->assertEquals(45, $json[Candidate::KEY_TOKEN_COUNT]); } /** @@ -367,15 +367,15 @@ public function testToArray(): void public function testFromArray(): void { $json = [ - 'message' => [ - 'role' => MessageRoleEnum::model()->value, - 'parts' => [ - ['type' => MessagePartTypeEnum::text()->value, 'text' => 'Response text 1'], - ['type' => MessagePartTypeEnum::text()->value, 'text' => 'Response text 2'] + Candidate::KEY_MESSAGE => [ + Message::KEY_ROLE => MessageRoleEnum::model()->value, + Message::KEY_PARTS => [ + [MessagePart::KEY_TYPE => MessagePartTypeEnum::text()->value, MessagePart::KEY_TEXT => 'Response text 1'], + [MessagePart::KEY_TYPE => MessagePartTypeEnum::text()->value, MessagePart::KEY_TEXT => 'Response text 2'] ] ], - 'finishReason' => FinishReasonEnum::stop()->value, - 'tokenCount' => 75 + Candidate::KEY_FINISH_REASON => FinishReasonEnum::stop()->value, + Candidate::KEY_TOKEN_COUNT => 75 ]; $candidate = Candidate::fromArray($json); diff --git a/tests/unit/Results/DTO/GenerativeAiResultTest.php b/tests/unit/Results/DTO/GenerativeAiResultTest.php index 9d65fe0b..798d34a3 100644 --- a/tests/unit/Results/DTO/GenerativeAiResultTest.php +++ b/tests/unit/Results/DTO/GenerativeAiResultTest.php @@ -530,30 +530,30 @@ public function testJsonSchema(): void // Check properties $this->assertArrayHasKey('properties', $schema); - $this->assertArrayHasKey('id', $schema['properties']); - $this->assertArrayHasKey('candidates', $schema['properties']); - $this->assertArrayHasKey('tokenUsage', $schema['properties']); - $this->assertArrayHasKey('providerMetadata', $schema['properties']); + $this->assertArrayHasKey(GenerativeAiResult::KEY_ID, $schema['properties']); + $this->assertArrayHasKey(GenerativeAiResult::KEY_CANDIDATES, $schema['properties']); + $this->assertArrayHasKey(GenerativeAiResult::KEY_TOKEN_USAGE, $schema['properties']); + $this->assertArrayHasKey(GenerativeAiResult::KEY_PROVIDER_METADATA, $schema['properties']); // Check id property - $this->assertEquals('string', $schema['properties']['id']['type']); + $this->assertEquals('string', $schema['properties'][GenerativeAiResult::KEY_ID]['type']); // Check candidates property - $candidatesSchema = $schema['properties']['candidates']; + $candidatesSchema = $schema['properties'][GenerativeAiResult::KEY_CANDIDATES]; $this->assertEquals('array', $candidatesSchema['type']); $this->assertEquals(1, $candidatesSchema['minItems']); // Check providerMetadata property - $metadataSchema = $schema['properties']['providerMetadata']; + $metadataSchema = $schema['properties'][GenerativeAiResult::KEY_PROVIDER_METADATA]; $this->assertEquals('object', $metadataSchema['type']); $this->assertTrue($metadataSchema['additionalProperties']); // Check required fields $this->assertArrayHasKey('required', $schema); - $this->assertContains('id', $schema['required']); - $this->assertContains('candidates', $schema['required']); - $this->assertContains('tokenUsage', $schema['required']); - $this->assertNotContains('providerMetadata', $schema['required']); + $this->assertContains(GenerativeAiResult::KEY_ID, $schema['required']); + $this->assertContains(GenerativeAiResult::KEY_CANDIDATES, $schema['required']); + $this->assertContains(GenerativeAiResult::KEY_TOKEN_USAGE, $schema['required']); + $this->assertNotContains(GenerativeAiResult::KEY_PROVIDER_METADATA, $schema['required']); } /** @@ -624,12 +624,12 @@ public function testToArray(): void $json = $this->assertToArrayReturnsArray($result); - $this->assertArrayHasKeys($json, ['id', 'candidates', 'tokenUsage', 'providerMetadata']); - $this->assertEquals('result_json_123', $json['id']); - $this->assertIsArray($json['candidates']); - $this->assertCount(1, $json['candidates']); - $this->assertIsArray($json['tokenUsage']); - $this->assertEquals($metadata, $json['providerMetadata']); + $this->assertArrayHasKeys($json, [GenerativeAiResult::KEY_ID, GenerativeAiResult::KEY_CANDIDATES, GenerativeAiResult::KEY_TOKEN_USAGE, GenerativeAiResult::KEY_PROVIDER_METADATA]); + $this->assertEquals('result_json_123', $json[GenerativeAiResult::KEY_ID]); + $this->assertIsArray($json[GenerativeAiResult::KEY_CANDIDATES]); + $this->assertCount(1, $json[GenerativeAiResult::KEY_CANDIDATES]); + $this->assertIsArray($json[GenerativeAiResult::KEY_TOKEN_USAGE]); + $this->assertEquals($metadata, $json[GenerativeAiResult::KEY_PROVIDER_METADATA]); } /** @@ -640,26 +640,26 @@ public function testToArray(): void public function testFromArray(): void { $json = [ - 'id' => 'result_from_json', - 'candidates' => [ + GenerativeAiResult::KEY_ID => 'result_from_json', + GenerativeAiResult::KEY_CANDIDATES => [ [ - 'message' => [ - 'role' => MessageRoleEnum::model()->value, - 'parts' => [ - ['type' => MessagePartTypeEnum::text()->value, 'text' => 'First part'], - ['type' => MessagePartTypeEnum::text()->value, 'text' => 'Second part'] + Candidate::KEY_MESSAGE => [ + Message::KEY_ROLE => MessageRoleEnum::model()->value, + Message::KEY_PARTS => [ + [MessagePart::KEY_TYPE => MessagePartTypeEnum::text()->value, MessagePart::KEY_TEXT => 'First part'], + [MessagePart::KEY_TYPE => MessagePartTypeEnum::text()->value, MessagePart::KEY_TEXT => 'Second part'] ] ], - 'finishReason' => FinishReasonEnum::stop()->value, - 'tokenCount' => 20 + Candidate::KEY_FINISH_REASON => FinishReasonEnum::stop()->value, + Candidate::KEY_TOKEN_COUNT => 20 ] ], - 'tokenUsage' => [ - 'promptTokens' => 8, - 'completionTokens' => 20, - 'totalTokens' => 28 + GenerativeAiResult::KEY_TOKEN_USAGE => [ + TokenUsage::KEY_PROMPT_TOKENS => 8, + TokenUsage::KEY_COMPLETION_TOKENS => 20, + TokenUsage::KEY_TOTAL_TOKENS => 28 ], - 'providerMetadata' => ['provider' => 'test'] + GenerativeAiResult::KEY_PROVIDER_METADATA => ['provider' => 'test'] ]; $result = GenerativeAiResult::fromArray($json); @@ -737,8 +737,8 @@ public function testToArrayWithoutProviderMetadata(): void $json = $this->assertToArrayReturnsArray($result); - $this->assertArrayHasKeys($json, ['id', 'candidates', 'tokenUsage', 'providerMetadata']); - $this->assertEquals([], $json['providerMetadata']); + $this->assertArrayHasKeys($json, [GenerativeAiResult::KEY_ID, GenerativeAiResult::KEY_CANDIDATES, GenerativeAiResult::KEY_TOKEN_USAGE, GenerativeAiResult::KEY_PROVIDER_METADATA]); + $this->assertEquals([], $json[GenerativeAiResult::KEY_PROVIDER_METADATA]); } /** diff --git a/tests/unit/Results/DTO/TokenUsageTest.php b/tests/unit/Results/DTO/TokenUsageTest.php index aed0cb60..7e8f7051 100644 --- a/tests/unit/Results/DTO/TokenUsageTest.php +++ b/tests/unit/Results/DTO/TokenUsageTest.php @@ -107,23 +107,23 @@ public function testJsonSchema(): void // Check properties $this->assertArrayHasKey('properties', $schema); - $this->assertArrayHasKey('promptTokens', $schema['properties']); - $this->assertArrayHasKey('completionTokens', $schema['properties']); - $this->assertArrayHasKey('totalTokens', $schema['properties']); + $this->assertArrayHasKey(TokenUsage::KEY_PROMPT_TOKENS, $schema['properties']); + $this->assertArrayHasKey(TokenUsage::KEY_COMPLETION_TOKENS, $schema['properties']); + $this->assertArrayHasKey(TokenUsage::KEY_TOTAL_TOKENS, $schema['properties']); // Check each property type - $this->assertEquals('integer', $schema['properties']['promptTokens']['type']); - $this->assertEquals('integer', $schema['properties']['completionTokens']['type']); - $this->assertEquals('integer', $schema['properties']['totalTokens']['type']); + $this->assertEquals('integer', $schema['properties'][TokenUsage::KEY_PROMPT_TOKENS]['type']); + $this->assertEquals('integer', $schema['properties'][TokenUsage::KEY_COMPLETION_TOKENS]['type']); + $this->assertEquals('integer', $schema['properties'][TokenUsage::KEY_TOTAL_TOKENS]['type']); // Check descriptions - $this->assertArrayHasKey('description', $schema['properties']['promptTokens']); - $this->assertArrayHasKey('description', $schema['properties']['completionTokens']); - $this->assertArrayHasKey('description', $schema['properties']['totalTokens']); + $this->assertArrayHasKey('description', $schema['properties'][TokenUsage::KEY_PROMPT_TOKENS]); + $this->assertArrayHasKey('description', $schema['properties'][TokenUsage::KEY_COMPLETION_TOKENS]); + $this->assertArrayHasKey('description', $schema['properties'][TokenUsage::KEY_TOTAL_TOKENS]); // Check required fields $this->assertArrayHasKey('required', $schema); - $this->assertEquals(['promptTokens', 'completionTokens', 'totalTokens'], $schema['required']); + $this->assertEquals([TokenUsage::KEY_PROMPT_TOKENS, TokenUsage::KEY_COMPLETION_TOKENS, TokenUsage::KEY_TOTAL_TOKENS], $schema['required']); } /** @@ -219,13 +219,13 @@ public function testToArray(): void $json = $tokenUsage->toArray(); $this->assertIsArray($json); - $this->assertArrayHasKey('promptTokens', $json); - $this->assertArrayHasKey('completionTokens', $json); - $this->assertArrayHasKey('totalTokens', $json); + $this->assertArrayHasKey(TokenUsage::KEY_PROMPT_TOKENS, $json); + $this->assertArrayHasKey(TokenUsage::KEY_COMPLETION_TOKENS, $json); + $this->assertArrayHasKey(TokenUsage::KEY_TOTAL_TOKENS, $json); - $this->assertEquals(100, $json['promptTokens']); - $this->assertEquals(50, $json['completionTokens']); - $this->assertEquals(150, $json['totalTokens']); + $this->assertEquals(100, $json[TokenUsage::KEY_PROMPT_TOKENS]); + $this->assertEquals(50, $json[TokenUsage::KEY_COMPLETION_TOKENS]); + $this->assertEquals(150, $json[TokenUsage::KEY_TOTAL_TOKENS]); } /** @@ -236,9 +236,9 @@ public function testToArray(): void public function testFromArray(): void { $json = [ - 'promptTokens' => 100, - 'completionTokens' => 50, - 'totalTokens' => 150, + TokenUsage::KEY_PROMPT_TOKENS => 100, + TokenUsage::KEY_COMPLETION_TOKENS => 50, + TokenUsage::KEY_TOTAL_TOKENS => 150, ]; $tokenUsage = TokenUsage::fromArray($json); diff --git a/tests/unit/Tools/DTO/FunctionCallTest.php b/tests/unit/Tools/DTO/FunctionCallTest.php index 25b7c175..c0d7dc91 100644 --- a/tests/unit/Tools/DTO/FunctionCallTest.php +++ b/tests/unit/Tools/DTO/FunctionCallTest.php @@ -105,34 +105,34 @@ public function testJsonSchema(): void // Check properties $this->assertArrayHasKey('properties', $schema); - $this->assertArrayHasKey('id', $schema['properties']); - $this->assertArrayHasKey('name', $schema['properties']); - $this->assertArrayHasKey('args', $schema['properties']); + $this->assertArrayHasKey(FunctionCall::KEY_ID, $schema['properties']); + $this->assertArrayHasKey(FunctionCall::KEY_NAME, $schema['properties']); + $this->assertArrayHasKey(FunctionCall::KEY_ARGS, $schema['properties']); // Check id property - $this->assertEquals('string', $schema['properties']['id']['type']); - $this->assertArrayHasKey('description', $schema['properties']['id']); + $this->assertEquals('string', $schema['properties'][FunctionCall::KEY_ID]['type']); + $this->assertArrayHasKey('description', $schema['properties'][FunctionCall::KEY_ID]); // Check name property - $this->assertEquals('string', $schema['properties']['name']['type']); - $this->assertArrayHasKey('description', $schema['properties']['name']); + $this->assertEquals('string', $schema['properties'][FunctionCall::KEY_NAME]['type']); + $this->assertArrayHasKey('description', $schema['properties'][FunctionCall::KEY_NAME]); // Check args property - $this->assertEquals('object', $schema['properties']['args']['type']); - $this->assertTrue($schema['properties']['args']['additionalProperties']); + $this->assertEquals('object', $schema['properties'][FunctionCall::KEY_ARGS]['type']); + $this->assertTrue($schema['properties'][FunctionCall::KEY_ARGS]['additionalProperties']); // Check oneOf for required fields $this->assertArrayHasKey('oneOf', $schema); $this->assertCount(3, $schema['oneOf']); // First option: only id required - $this->assertEquals(['id'], $schema['oneOf'][0]['required']); + $this->assertEquals([FunctionCall::KEY_ID], $schema['oneOf'][0]['required']); // Second option: only name required - $this->assertEquals(['name'], $schema['oneOf'][1]['required']); + $this->assertEquals([FunctionCall::KEY_NAME], $schema['oneOf'][1]['required']); // Third option: both id and name required - $this->assertEquals(['id', 'name'], $schema['oneOf'][2]['required']); + $this->assertEquals([FunctionCall::KEY_ID, FunctionCall::KEY_NAME], $schema['oneOf'][2]['required']); } /** @@ -173,9 +173,9 @@ public function testToArrayAllFields(): void $json = $functionCall->toArray(); $this->assertIsArray($json); - $this->assertEquals('func_123', $json['id']); - $this->assertEquals('calculate', $json['name']); - $this->assertEquals(['x' => 10, 'y' => 20], $json['args']); + $this->assertEquals('func_123', $json[FunctionCall::KEY_ID]); + $this->assertEquals('calculate', $json[FunctionCall::KEY_NAME]); + $this->assertEquals(['x' => 10, 'y' => 20], $json[FunctionCall::KEY_ARGS]); } /** @@ -189,9 +189,9 @@ public function testToArrayOnlyId(): void $json = $functionCall->toArray(); $this->assertIsArray($json); - $this->assertEquals('func_456', $json['id']); - $this->assertArrayNotHasKey('name', $json); - $this->assertArrayNotHasKey('args', $json); + $this->assertEquals('func_456', $json[FunctionCall::KEY_ID]); + $this->assertArrayNotHasKey(FunctionCall::KEY_NAME, $json); + $this->assertArrayNotHasKey(FunctionCall::KEY_ARGS, $json); } /** @@ -205,9 +205,9 @@ public function testToArrayOnlyName(): void $json = $functionCall->toArray(); $this->assertIsArray($json); - $this->assertEquals('search', $json['name']); - $this->assertArrayNotHasKey('id', $json); - $this->assertArrayNotHasKey('args', $json); + $this->assertEquals('search', $json[FunctionCall::KEY_NAME]); + $this->assertArrayNotHasKey(FunctionCall::KEY_ID, $json); + $this->assertArrayNotHasKey(FunctionCall::KEY_ARGS, $json); } /** @@ -218,9 +218,9 @@ public function testToArrayOnlyName(): void public function testFromArrayAllFields(): void { $json = [ - 'id' => 'func_789', - 'name' => 'process', - 'args' => ['input' => 'data', 'format' => 'json'] + FunctionCall::KEY_ID => 'func_789', + FunctionCall::KEY_NAME => 'process', + FunctionCall::KEY_ARGS => ['input' => 'data', 'format' => 'json'] ]; $functionCall = FunctionCall::fromArray($json); @@ -238,7 +238,7 @@ public function testFromArrayAllFields(): void */ public function testFromArrayMinimalFields(): void { - $json = ['name' => 'minimal']; + $json = [FunctionCall::KEY_NAME => 'minimal']; $functionCall = FunctionCall::fromArray($json); diff --git a/tests/unit/Tools/DTO/FunctionDeclarationTest.php b/tests/unit/Tools/DTO/FunctionDeclarationTest.php index c18ebb97..cc62d8cc 100644 --- a/tests/unit/Tools/DTO/FunctionDeclarationTest.php +++ b/tests/unit/Tools/DTO/FunctionDeclarationTest.php @@ -113,20 +113,20 @@ public function testJsonSchema(): void // Check properties $this->assertArrayHasKey('properties', $schema); - $this->assertArrayHasKey('name', $schema['properties']); - $this->assertArrayHasKey('description', $schema['properties']); - $this->assertArrayHasKey('parameters', $schema['properties']); + $this->assertArrayHasKey(FunctionDeclaration::KEY_NAME, $schema['properties']); + $this->assertArrayHasKey(FunctionDeclaration::KEY_DESCRIPTION, $schema['properties']); + $this->assertArrayHasKey(FunctionDeclaration::KEY_PARAMETERS, $schema['properties']); // Check name property - $this->assertEquals('string', $schema['properties']['name']['type']); - $this->assertArrayHasKey('description', $schema['properties']['name']); + $this->assertEquals('string', $schema['properties'][FunctionDeclaration::KEY_NAME]['type']); + $this->assertArrayHasKey('description', $schema['properties'][FunctionDeclaration::KEY_NAME]); // Check description property - $this->assertEquals('string', $schema['properties']['description']['type']); - $this->assertArrayHasKey('description', $schema['properties']['description']); + $this->assertEquals('string', $schema['properties'][FunctionDeclaration::KEY_DESCRIPTION]['type']); + $this->assertArrayHasKey('description', $schema['properties'][FunctionDeclaration::KEY_DESCRIPTION]); // Check parameters property allows multiple types - $paramTypes = $schema['properties']['parameters']['type']; + $paramTypes = $schema['properties'][FunctionDeclaration::KEY_PARAMETERS]['type']; $this->assertIsArray($paramTypes); $this->assertContains('string', $paramTypes); $this->assertContains('number', $paramTypes); @@ -137,8 +137,8 @@ public function testJsonSchema(): void // Check required fields - parameters should NOT be required $this->assertArrayHasKey('required', $schema); - $this->assertEquals(['name', 'description'], $schema['required']); - $this->assertNotContains('parameters', $schema['required']); + $this->assertEquals([FunctionDeclaration::KEY_NAME, FunctionDeclaration::KEY_DESCRIPTION], $schema['required']); + $this->assertNotContains(FunctionDeclaration::KEY_PARAMETERS, $schema['required']); } /** @@ -203,10 +203,10 @@ public function testToArrayWithParameters(): void $json = $this->assertToArrayReturnsArray($declaration); - $this->assertArrayHasKeys($json, ['name', 'description', 'parameters']); - $this->assertEquals('searchWeb', $json['name']); - $this->assertEquals('Searches the web for information', $json['description']); - $this->assertEquals(['type' => 'object', 'properties' => ['query' => ['type' => 'string']]], $json['parameters']); + $this->assertArrayHasKeys($json, [FunctionDeclaration::KEY_NAME, FunctionDeclaration::KEY_DESCRIPTION, FunctionDeclaration::KEY_PARAMETERS]); + $this->assertEquals('searchWeb', $json[FunctionDeclaration::KEY_NAME]); + $this->assertEquals('Searches the web for information', $json[FunctionDeclaration::KEY_DESCRIPTION]); + $this->assertEquals(['type' => 'object', 'properties' => ['query' => ['type' => 'string']]], $json[FunctionDeclaration::KEY_PARAMETERS]); } /** @@ -223,10 +223,10 @@ public function testToArrayWithoutParameters(): void $json = $this->assertToArrayReturnsArray($declaration); - $this->assertArrayHasKeys($json, ['name', 'description']); - $this->assertArrayNotHasKey('parameters', $json); - $this->assertEquals('getTimestamp', $json['name']); - $this->assertEquals('Returns the current Unix timestamp', $json['description']); + $this->assertArrayHasKeys($json, [FunctionDeclaration::KEY_NAME, FunctionDeclaration::KEY_DESCRIPTION]); + $this->assertArrayNotHasKey(FunctionDeclaration::KEY_PARAMETERS, $json); + $this->assertEquals('getTimestamp', $json[FunctionDeclaration::KEY_NAME]); + $this->assertEquals('Returns the current Unix timestamp', $json[FunctionDeclaration::KEY_DESCRIPTION]); } /** @@ -237,9 +237,9 @@ public function testToArrayWithoutParameters(): void public function testFromArrayWithParameters(): void { $json = [ - 'name' => 'calculateArea', - 'description' => 'Calculates the area of a rectangle', - 'parameters' => [ + FunctionDeclaration::KEY_NAME => 'calculateArea', + FunctionDeclaration::KEY_DESCRIPTION => 'Calculates the area of a rectangle', + FunctionDeclaration::KEY_PARAMETERS => [ 'type' => 'object', 'properties' => [ 'width' => ['type' => 'number'], @@ -254,7 +254,7 @@ public function testFromArrayWithParameters(): void $this->assertInstanceOf(FunctionDeclaration::class, $declaration); $this->assertEquals('calculateArea', $declaration->getName()); $this->assertEquals('Calculates the area of a rectangle', $declaration->getDescription()); - $this->assertEquals($json['parameters'], $declaration->getParameters()); + $this->assertEquals($json[FunctionDeclaration::KEY_PARAMETERS], $declaration->getParameters()); } /** @@ -265,8 +265,8 @@ public function testFromArrayWithParameters(): void public function testFromArrayWithoutParameters(): void { $json = [ - 'name' => 'ping', - 'description' => 'Simple ping function' + FunctionDeclaration::KEY_NAME => 'ping', + FunctionDeclaration::KEY_DESCRIPTION => 'Simple ping function' ]; $declaration = FunctionDeclaration::fromArray($json); diff --git a/tests/unit/Tools/DTO/FunctionResponseTest.php b/tests/unit/Tools/DTO/FunctionResponseTest.php index 5606ef89..6fd66a87 100644 --- a/tests/unit/Tools/DTO/FunctionResponseTest.php +++ b/tests/unit/Tools/DTO/FunctionResponseTest.php @@ -100,20 +100,20 @@ public function testJsonSchema(): void // Check properties $this->assertArrayHasKey('properties', $schema); - $this->assertArrayHasKey('id', $schema['properties']); - $this->assertArrayHasKey('name', $schema['properties']); - $this->assertArrayHasKey('response', $schema['properties']); + $this->assertArrayHasKey(FunctionResponse::KEY_ID, $schema['properties']); + $this->assertArrayHasKey(FunctionResponse::KEY_NAME, $schema['properties']); + $this->assertArrayHasKey(FunctionResponse::KEY_RESPONSE, $schema['properties']); // Check id property - $this->assertEquals('string', $schema['properties']['id']['type']); - $this->assertArrayHasKey('description', $schema['properties']['id']); + $this->assertEquals('string', $schema['properties'][FunctionResponse::KEY_ID]['type']); + $this->assertArrayHasKey('description', $schema['properties'][FunctionResponse::KEY_ID]); // Check name property - $this->assertEquals('string', $schema['properties']['name']['type']); - $this->assertArrayHasKey('description', $schema['properties']['name']); + $this->assertEquals('string', $schema['properties'][FunctionResponse::KEY_NAME]['type']); + $this->assertArrayHasKey('description', $schema['properties'][FunctionResponse::KEY_NAME]); // Check response property allows multiple types - $responseTypes = $schema['properties']['response']['type']; + $responseTypes = $schema['properties'][FunctionResponse::KEY_RESPONSE]['type']; $this->assertIsArray($responseTypes); $this->assertContains('string', $responseTypes); $this->assertContains('number', $responseTypes); @@ -124,7 +124,7 @@ public function testJsonSchema(): void // Check required fields $this->assertArrayHasKey('required', $schema); - $this->assertEquals(['id', 'name', 'response'], $schema['required']); + $this->assertEquals([FunctionResponse::KEY_ID, FunctionResponse::KEY_NAME, FunctionResponse::KEY_RESPONSE], $schema['required']); } /** @@ -194,10 +194,10 @@ public function testToArray(): void $response = new FunctionResponse('func_123', 'calculate', ['result' => 42]); $json = $this->assertToArrayReturnsArray($response); - $this->assertArrayHasKeys($json, ['id', 'name', 'response']); - $this->assertEquals('func_123', $json['id']); - $this->assertEquals('calculate', $json['name']); - $this->assertEquals(['result' => 42], $json['response']); + $this->assertArrayHasKeys($json, [FunctionResponse::KEY_ID, FunctionResponse::KEY_NAME, FunctionResponse::KEY_RESPONSE]); + $this->assertEquals('func_123', $json[FunctionResponse::KEY_ID]); + $this->assertEquals('calculate', $json[FunctionResponse::KEY_NAME]); + $this->assertEquals(['result' => 42], $json[FunctionResponse::KEY_RESPONSE]); } /** @@ -208,9 +208,9 @@ public function testToArray(): void public function testFromArray(): void { $json = [ - 'id' => 'func_456', - 'name' => 'search', - 'response' => ['found' => true, 'count' => 5] + FunctionResponse::KEY_ID => 'func_456', + FunctionResponse::KEY_NAME => 'search', + FunctionResponse::KEY_RESPONSE => ['found' => true, 'count' => 5] ]; $response = FunctionResponse::fromArray($json); diff --git a/tests/unit/Tools/DTO/ToolTest.php b/tests/unit/Tools/DTO/ToolTest.php index e951023b..9ebf2833 100644 --- a/tests/unit/Tools/DTO/ToolTest.php +++ b/tests/unit/Tools/DTO/ToolTest.php @@ -143,21 +143,21 @@ public function testJsonSchemaForFunctionDeclarationsTool(): void $functionSchema = $schema['oneOf'][0]; $this->assertEquals('object', $functionSchema['type']); $this->assertArrayHasKey('properties', $functionSchema); - $this->assertArrayHasKey('type', $functionSchema['properties']); - $this->assertArrayHasKey('functionDeclarations', $functionSchema['properties']); + $this->assertArrayHasKey(Tool::KEY_TYPE, $functionSchema['properties']); + $this->assertArrayHasKey(Tool::KEY_FUNCTION_DECLARATIONS, $functionSchema['properties']); // Type property - $typeProperty = $functionSchema['properties']['type']; + $typeProperty = $functionSchema['properties'][Tool::KEY_TYPE]; $this->assertEquals('string', $typeProperty['type']); $this->assertEquals(ToolTypeEnum::functionDeclarations()->value, $typeProperty['const']); // Function declarations property - $functionsProperty = $functionSchema['properties']['functionDeclarations']; + $functionsProperty = $functionSchema['properties'][Tool::KEY_FUNCTION_DECLARATIONS]; $this->assertEquals('array', $functionsProperty['type']); $this->assertArrayHasKey('items', $functionsProperty); // Required fields - $this->assertEquals(['type', 'functionDeclarations'], $functionSchema['required']); + $this->assertEquals([Tool::KEY_TYPE, Tool::KEY_FUNCTION_DECLARATIONS], $functionSchema['required']); } /** @@ -173,19 +173,19 @@ public function testJsonSchemaForWebSearchTool(): void $webSearchSchema = $schema['oneOf'][1]; $this->assertEquals('object', $webSearchSchema['type']); $this->assertArrayHasKey('properties', $webSearchSchema); - $this->assertArrayHasKey('type', $webSearchSchema['properties']); - $this->assertArrayHasKey('webSearch', $webSearchSchema['properties']); + $this->assertArrayHasKey(Tool::KEY_TYPE, $webSearchSchema['properties']); + $this->assertArrayHasKey(Tool::KEY_WEB_SEARCH, $webSearchSchema['properties']); // Type property - $typeProperty = $webSearchSchema['properties']['type']; + $typeProperty = $webSearchSchema['properties'][Tool::KEY_TYPE]; $this->assertEquals('string', $typeProperty['type']); $this->assertEquals(ToolTypeEnum::webSearch()->value, $typeProperty['const']); // Web search property - $this->assertArrayHasKey('webSearch', $webSearchSchema['properties']); + $this->assertArrayHasKey(Tool::KEY_WEB_SEARCH, $webSearchSchema['properties']); // Required fields - $this->assertEquals(['type', 'webSearch'], $webSearchSchema['required']); + $this->assertEquals([Tool::KEY_TYPE, Tool::KEY_WEB_SEARCH], $webSearchSchema['required']); } /** @@ -313,12 +313,12 @@ public function testToArrayWithFunctionDeclarations(): void $tool = new Tool($functions); $json = $this->assertToArrayReturnsArray($tool); - $this->assertArrayHasKeys($json, ['type', 'functionDeclarations']); - $this->assertEquals(ToolTypeEnum::functionDeclarations()->value, $json['type']); - $this->assertIsArray($json['functionDeclarations']); - $this->assertCount(2, $json['functionDeclarations']); - $this->assertEquals('func1', $json['functionDeclarations'][0]['name']); - $this->assertEquals('func2', $json['functionDeclarations'][1]['name']); + $this->assertArrayHasKeys($json, [Tool::KEY_TYPE, Tool::KEY_FUNCTION_DECLARATIONS]); + $this->assertEquals(ToolTypeEnum::functionDeclarations()->value, $json[Tool::KEY_TYPE]); + $this->assertIsArray($json[Tool::KEY_FUNCTION_DECLARATIONS]); + $this->assertCount(2, $json[Tool::KEY_FUNCTION_DECLARATIONS]); + $this->assertEquals('func1', $json[Tool::KEY_FUNCTION_DECLARATIONS][0][FunctionDeclaration::KEY_NAME]); + $this->assertEquals('func2', $json[Tool::KEY_FUNCTION_DECLARATIONS][1][FunctionDeclaration::KEY_NAME]); } /** @@ -336,11 +336,11 @@ public function testToArrayWithWebSearch(): void $tool = new Tool($webSearch); $json = $this->assertToArrayReturnsArray($tool); - $this->assertArrayHasKeys($json, ['type', 'webSearch']); - $this->assertEquals(ToolTypeEnum::webSearch()->value, $json['type']); - $this->assertIsArray($json['webSearch']); - $this->assertArrayHasKey('allowedDomains', $json['webSearch']); - $this->assertArrayHasKey('disallowedDomains', $json['webSearch']); + $this->assertArrayHasKeys($json, [Tool::KEY_TYPE, Tool::KEY_WEB_SEARCH]); + $this->assertEquals(ToolTypeEnum::webSearch()->value, $json[Tool::KEY_TYPE]); + $this->assertIsArray($json[Tool::KEY_WEB_SEARCH]); + $this->assertArrayHasKey(WebSearch::KEY_ALLOWED_DOMAINS, $json[Tool::KEY_WEB_SEARCH]); + $this->assertArrayHasKey(WebSearch::KEY_DISALLOWED_DOMAINS, $json[Tool::KEY_WEB_SEARCH]); } /** @@ -351,12 +351,12 @@ public function testToArrayWithWebSearch(): void public function testFromArrayWithFunctionDeclarations(): void { $json = [ - 'type' => ToolTypeEnum::functionDeclarations()->value, - 'functionDeclarations' => [ + Tool::KEY_TYPE => ToolTypeEnum::functionDeclarations()->value, + Tool::KEY_FUNCTION_DECLARATIONS => [ [ - 'name' => 'testFunc', - 'description' => 'Test function', - 'parameters' => ['type' => 'object'] + FunctionDeclaration::KEY_NAME => 'testFunc', + FunctionDeclaration::KEY_DESCRIPTION => 'Test function', + FunctionDeclaration::KEY_PARAMETERS => ['type' => 'object'] ] ] ]; @@ -378,10 +378,10 @@ public function testFromArrayWithFunctionDeclarations(): void public function testFromArrayWithWebSearch(): void { $json = [ - 'type' => ToolTypeEnum::webSearch()->value, - 'webSearch' => [ - 'allowedDomains' => ['example.com'], - 'disallowedDomains' => ['spam.com'] + Tool::KEY_TYPE => ToolTypeEnum::webSearch()->value, + Tool::KEY_WEB_SEARCH => [ + WebSearch::KEY_ALLOWED_DOMAINS => ['example.com'], + WebSearch::KEY_DISALLOWED_DOMAINS => ['spam.com'] ] ]; diff --git a/tests/unit/Tools/DTO/WebSearchTest.php b/tests/unit/Tools/DTO/WebSearchTest.php index cb060251..2016d347 100644 --- a/tests/unit/Tools/DTO/WebSearchTest.php +++ b/tests/unit/Tools/DTO/WebSearchTest.php @@ -128,18 +128,18 @@ public function testJsonSchema(): void // Check properties $this->assertArrayHasKey('properties', $schema); - $this->assertArrayHasKey('allowedDomains', $schema['properties']); - $this->assertArrayHasKey('disallowedDomains', $schema['properties']); + $this->assertArrayHasKey(WebSearch::KEY_ALLOWED_DOMAINS, $schema['properties']); + $this->assertArrayHasKey(WebSearch::KEY_DISALLOWED_DOMAINS, $schema['properties']); // Check allowedDomains property - $allowedSchema = $schema['properties']['allowedDomains']; + $allowedSchema = $schema['properties'][WebSearch::KEY_ALLOWED_DOMAINS]; $this->assertEquals('array', $allowedSchema['type']); $this->assertArrayHasKey('items', $allowedSchema); $this->assertEquals('string', $allowedSchema['items']['type']); $this->assertArrayHasKey('description', $allowedSchema); // Check disallowedDomains property - $disallowedSchema = $schema['properties']['disallowedDomains']; + $disallowedSchema = $schema['properties'][WebSearch::KEY_DISALLOWED_DOMAINS]; $this->assertEquals('array', $disallowedSchema['type']); $this->assertArrayHasKey('items', $disallowedSchema); $this->assertEquals('string', $disallowedSchema['items']['type']); @@ -308,9 +308,9 @@ public function testToArrayWithBothDomainLists(): void $json = $this->assertToArrayReturnsArray($webSearch); - $this->assertArrayHasKeys($json, ['allowedDomains', 'disallowedDomains']); - $this->assertEquals(['example.com', 'docs.example.com'], $json['allowedDomains']); - $this->assertEquals(['spam.com', 'malware.com'], $json['disallowedDomains']); + $this->assertArrayHasKeys($json, [WebSearch::KEY_ALLOWED_DOMAINS, WebSearch::KEY_DISALLOWED_DOMAINS]); + $this->assertEquals(['example.com', 'docs.example.com'], $json[WebSearch::KEY_ALLOWED_DOMAINS]); + $this->assertEquals(['spam.com', 'malware.com'], $json[WebSearch::KEY_DISALLOWED_DOMAINS]); } /** @@ -324,9 +324,9 @@ public function testToArrayWithEmptyDomainLists(): void $json = $this->assertToArrayReturnsArray($webSearch); - $this->assertArrayHasKeys($json, ['allowedDomains', 'disallowedDomains']); - $this->assertEquals([], $json['allowedDomains']); - $this->assertEquals([], $json['disallowedDomains']); + $this->assertArrayHasKeys($json, [WebSearch::KEY_ALLOWED_DOMAINS, WebSearch::KEY_DISALLOWED_DOMAINS]); + $this->assertEquals([], $json[WebSearch::KEY_ALLOWED_DOMAINS]); + $this->assertEquals([], $json[WebSearch::KEY_DISALLOWED_DOMAINS]); } /** @@ -340,9 +340,9 @@ public function testToArrayWithOnlyAllowedDomains(): void $json = $this->assertToArrayReturnsArray($webSearch); - $this->assertArrayHasKeys($json, ['allowedDomains', 'disallowedDomains']); - $this->assertEquals(['trusted1.com', 'trusted2.com'], $json['allowedDomains']); - $this->assertEquals([], $json['disallowedDomains']); + $this->assertArrayHasKeys($json, [WebSearch::KEY_ALLOWED_DOMAINS, WebSearch::KEY_DISALLOWED_DOMAINS]); + $this->assertEquals(['trusted1.com', 'trusted2.com'], $json[WebSearch::KEY_ALLOWED_DOMAINS]); + $this->assertEquals([], $json[WebSearch::KEY_DISALLOWED_DOMAINS]); } /** @@ -353,8 +353,8 @@ public function testToArrayWithOnlyAllowedDomains(): void public function testFromArrayWithBothDomainLists(): void { $json = [ - 'allowedDomains' => ['api.example.com', 'docs.example.com'], - 'disallowedDomains' => ['ads.example.com', 'tracking.example.com'] + WebSearch::KEY_ALLOWED_DOMAINS => ['api.example.com', 'docs.example.com'], + WebSearch::KEY_DISALLOWED_DOMAINS => ['ads.example.com', 'tracking.example.com'] ]; $webSearch = WebSearch::fromArray($json); @@ -372,8 +372,8 @@ public function testFromArrayWithBothDomainLists(): void public function testFromArrayWithEmptyArrays(): void { $json = [ - 'allowedDomains' => [], - 'disallowedDomains' => [] + WebSearch::KEY_ALLOWED_DOMAINS => [], + WebSearch::KEY_DISALLOWED_DOMAINS => [] ]; $webSearch = WebSearch::fromArray($json); From 59514c33605216e790e62ceece181e613e7fcc3c Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 15:51:20 -0600 Subject: [PATCH 27/36] test: adds missing import --- tests/unit/Results/DTO/GenerativeAiResultTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/Results/DTO/GenerativeAiResultTest.php b/tests/unit/Results/DTO/GenerativeAiResultTest.php index 798d34a3..35772b55 100644 --- a/tests/unit/Results/DTO/GenerativeAiResultTest.php +++ b/tests/unit/Results/DTO/GenerativeAiResultTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Files\DTO\File; +use WordPress\AiClient\Messages\DTO\Message; use WordPress\AiClient\Messages\DTO\MessagePart; use WordPress\AiClient\Messages\DTO\ModelMessage; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; From 59e586cf739ad8a46a9e9778945c4e2a3ac5c09d Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 16:20:12 -0600 Subject: [PATCH 28/36] refactor: removes need for final DTOs --- .../WithArrayTransformationInterface.php | 4 ++-- src/Files/DTO/File.php | 4 ++-- src/Messages/DTO/Message.php | 4 ++-- src/Messages/DTO/MessagePart.php | 4 ++-- src/Messages/DTO/ModelMessage.php | 2 +- src/Messages/DTO/SystemMessage.php | 2 +- src/Messages/DTO/UserMessage.php | 2 +- src/Operations/DTO/GenerativeAiOperation.php | 4 ++-- src/Results/DTO/Candidate.php | 4 ++-- src/Results/DTO/GenerativeAiResult.php | 4 ++-- src/Results/DTO/TokenUsage.php | 4 ++-- src/Tools/DTO/FunctionCall.php | 4 ++-- src/Tools/DTO/FunctionDeclaration.php | 4 ++-- src/Tools/DTO/FunctionResponse.php | 4 ++-- src/Tools/DTO/Tool.php | 4 ++-- src/Tools/DTO/WebSearch.php | 4 ++-- .../unit/Common/AbstractDataValueObjectTest.php | 16 ++++++++-------- 17 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/Common/Contracts/WithArrayTransformationInterface.php b/src/Common/Contracts/WithArrayTransformationInterface.php index c0b3ffeb..f867d773 100644 --- a/src/Common/Contracts/WithArrayTransformationInterface.php +++ b/src/Common/Contracts/WithArrayTransformationInterface.php @@ -28,7 +28,7 @@ public function toArray(): array; * @since 1.0.0 * * @param TArrayShape $array The array data. - * @return static The created instance. + * @return self The created instance. */ - public static function fromArray(array $array); + public static function fromArray(array $array): self; } diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index 2e81f95c..475f7e18 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -27,7 +27,7 @@ * * @extends AbstractDataValueObject */ -final class File extends AbstractDataValueObject +class File extends AbstractDataValueObject { public const KEY_FILE_TYPE = 'fileType'; public const KEY_MIME_TYPE = 'mimeType'; @@ -425,7 +425,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): File + public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_FILE_TYPE]); diff --git a/src/Messages/DTO/Message.php b/src/Messages/DTO/Message.php index 445751b8..f38e0149 100644 --- a/src/Messages/DTO/Message.php +++ b/src/Messages/DTO/Message.php @@ -125,9 +125,9 @@ public function toArray(): array * * @since n.e.x.t * - * @return Message The specific message class based on the role. + * @return self The specific message class based on the role. */ - final public static function fromArray(array $array): Message + final public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_ROLE, self::KEY_PARTS]); diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index edc9b37d..abd3e4a2 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -34,7 +34,7 @@ * * @extends AbstractDataValueObject */ -final class MessagePart extends AbstractDataValueObject +class MessagePart extends AbstractDataValueObject { public const KEY_TYPE = 'type'; public const KEY_TEXT = 'text'; @@ -258,7 +258,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): MessagePart + public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_TYPE]); diff --git a/src/Messages/DTO/ModelMessage.php b/src/Messages/DTO/ModelMessage.php index 99f884cb..a9f4500f 100644 --- a/src/Messages/DTO/ModelMessage.php +++ b/src/Messages/DTO/ModelMessage.php @@ -15,7 +15,7 @@ * * @since n.e.x.t */ -final class ModelMessage extends Message +class ModelMessage extends Message { /** * Constructor. diff --git a/src/Messages/DTO/SystemMessage.php b/src/Messages/DTO/SystemMessage.php index 0997789f..193999d4 100644 --- a/src/Messages/DTO/SystemMessage.php +++ b/src/Messages/DTO/SystemMessage.php @@ -15,7 +15,7 @@ * * @since n.e.x.t */ -final class SystemMessage extends Message +class SystemMessage extends Message { /** * Constructor. diff --git a/src/Messages/DTO/UserMessage.php b/src/Messages/DTO/UserMessage.php index 8ae2965a..42b97a65 100644 --- a/src/Messages/DTO/UserMessage.php +++ b/src/Messages/DTO/UserMessage.php @@ -14,7 +14,7 @@ * * @since n.e.x.t */ -final class UserMessage extends Message +class UserMessage extends Message { /** * Constructor. diff --git a/src/Operations/DTO/GenerativeAiOperation.php b/src/Operations/DTO/GenerativeAiOperation.php index 94d10419..01951423 100644 --- a/src/Operations/DTO/GenerativeAiOperation.php +++ b/src/Operations/DTO/GenerativeAiOperation.php @@ -24,7 +24,7 @@ * * @extends AbstractDataValueObject */ -final class GenerativeAiOperation extends AbstractDataValueObject implements OperationInterface +class GenerativeAiOperation extends AbstractDataValueObject implements OperationInterface { public const KEY_ID = 'id'; public const KEY_STATE = 'state'; @@ -170,7 +170,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): GenerativeAiOperation + public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_ID, self::KEY_STATE]); diff --git a/src/Results/DTO/Candidate.php b/src/Results/DTO/Candidate.php index 04dfffd2..7f85920d 100644 --- a/src/Results/DTO/Candidate.php +++ b/src/Results/DTO/Candidate.php @@ -22,7 +22,7 @@ * * @extends AbstractDataValueObject */ -final class Candidate extends AbstractDataValueObject +class Candidate extends AbstractDataValueObject { public const KEY_MESSAGE = 'message'; public const KEY_FINISH_REASON = 'finishReason'; @@ -146,7 +146,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): Candidate + public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_MESSAGE, self::KEY_FINISH_REASON, self::KEY_TOKEN_COUNT]); diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index 9125ff9a..ed1c586d 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -29,7 +29,7 @@ * * @extends AbstractDataValueObject */ -final class GenerativeAiResult extends AbstractDataValueObject implements ResultInterface +class GenerativeAiResult extends AbstractDataValueObject implements ResultInterface { public const KEY_ID = 'id'; public const KEY_CANDIDATES = 'candidates'; @@ -417,7 +417,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): GenerativeAiResult + public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE]); diff --git a/src/Results/DTO/TokenUsage.php b/src/Results/DTO/TokenUsage.php index cbd3774d..6e30694d 100644 --- a/src/Results/DTO/TokenUsage.php +++ b/src/Results/DTO/TokenUsage.php @@ -22,7 +22,7 @@ * * @extends AbstractDataValueObject */ -final class TokenUsage extends AbstractDataValueObject +class TokenUsage extends AbstractDataValueObject { public const KEY_PROMPT_TOKENS = 'promptTokens'; public const KEY_COMPLETION_TOKENS = 'completionTokens'; @@ -142,7 +142,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): TokenUsage + public static function fromArray(array $array): self { static::validateFromArrayData($array, [ self::KEY_PROMPT_TOKENS, diff --git a/src/Tools/DTO/FunctionCall.php b/src/Tools/DTO/FunctionCall.php index b5adda57..9722c0ec 100644 --- a/src/Tools/DTO/FunctionCall.php +++ b/src/Tools/DTO/FunctionCall.php @@ -18,7 +18,7 @@ * * @extends AbstractDataValueObject */ -final class FunctionCall extends AbstractDataValueObject +class FunctionCall extends AbstractDataValueObject { public const KEY_ID = 'id'; public const KEY_NAME = 'name'; @@ -164,7 +164,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): FunctionCall + public static function fromArray(array $array): self { return new self( $array[self::KEY_ID] ?? null, diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index 293d4e5a..8b14d8e4 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -19,7 +19,7 @@ * * @extends AbstractDataValueObject */ -final class FunctionDeclaration extends AbstractDataValueObject +class FunctionDeclaration extends AbstractDataValueObject { public const KEY_NAME = 'name'; public const KEY_DESCRIPTION = 'description'; @@ -144,7 +144,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): FunctionDeclaration + public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_NAME, self::KEY_DESCRIPTION]); diff --git a/src/Tools/DTO/FunctionResponse.php b/src/Tools/DTO/FunctionResponse.php index ba4bf0f6..e1b6ef48 100644 --- a/src/Tools/DTO/FunctionResponse.php +++ b/src/Tools/DTO/FunctionResponse.php @@ -18,7 +18,7 @@ * * @extends AbstractDataValueObject */ -final class FunctionResponse extends AbstractDataValueObject +class FunctionResponse extends AbstractDataValueObject { public const KEY_ID = 'id'; public const KEY_NAME = 'name'; @@ -138,7 +138,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): FunctionResponse + public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_ID, self::KEY_NAME, self::KEY_RESPONSE]); diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index 9bb69bce..1b335c11 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -27,7 +27,7 @@ * * @extends AbstractDataValueObject */ -final class Tool extends AbstractDataValueObject +class Tool extends AbstractDataValueObject { public const KEY_TYPE = 'type'; public const KEY_FUNCTION_DECLARATIONS = 'functionDeclarations'; @@ -174,7 +174,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): Tool + public static function fromArray(array $array): self { static::validateFromArrayData($array, [self::KEY_TYPE]); diff --git a/src/Tools/DTO/WebSearch.php b/src/Tools/DTO/WebSearch.php index 52fcdffa..fe01c7d7 100644 --- a/src/Tools/DTO/WebSearch.php +++ b/src/Tools/DTO/WebSearch.php @@ -18,7 +18,7 @@ * * @extends AbstractDataValueObject */ -final class WebSearch extends AbstractDataValueObject +class WebSearch extends AbstractDataValueObject { public const KEY_ALLOWED_DOMAINS = 'allowedDomains'; public const KEY_DISALLOWED_DOMAINS = 'disallowedDomains'; @@ -119,7 +119,7 @@ public function toArray(): array * * @since n.e.x.t */ - public static function fromArray(array $array): WebSearch + public static function fromArray(array $array): self { return new self( $array[self::KEY_ALLOWED_DOMAINS] ?? [], diff --git a/tests/unit/Common/AbstractDataValueObjectTest.php b/tests/unit/Common/AbstractDataValueObjectTest.php index e89a9d20..4c9e28b7 100644 --- a/tests/unit/Common/AbstractDataValueObjectTest.php +++ b/tests/unit/Common/AbstractDataValueObjectTest.php @@ -38,7 +38,7 @@ public function toArray(): array ]; } - public static function fromArray(array $array) + public static function fromArray(array $array): self { return new static(); } @@ -119,7 +119,7 @@ public function toArray(): array ]; } - public static function fromArray(array $array) + public static function fromArray(array $array): self { return new static(); } @@ -180,7 +180,7 @@ public function toArray(): array ]; } - public static function fromArray(array $array) + public static function fromArray(array $array): self { return new static(); } @@ -261,7 +261,7 @@ public function toArray(): array ]; } - public static function fromArray(array $array) + public static function fromArray(array $array): self { return new static(); } @@ -335,7 +335,7 @@ public function toArray(): array ]; } - public static function fromArray(array $array) + public static function fromArray(array $array): self { return new static(); } @@ -412,7 +412,7 @@ public function toArray(): array ]; } - public static function fromArray(array $array) + public static function fromArray(array $array): self { return new static(); } @@ -476,7 +476,7 @@ public function toArray(): array ]; } - public static function fromArray(array $array) + public static function fromArray(array $array): self { return new static(); } @@ -527,7 +527,7 @@ public function toArray(): array return ['test' => 'value']; } - public static function fromArray(array $array) + public static function fromArray(array $array): self { return new static(); } From c4c6452fc84bbc6be93143333a61c348aff5b724 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 16:38:45 -0600 Subject: [PATCH 29/36] fix: checks for key existence, allowing null values --- src/Common/AbstractDataValueObject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/AbstractDataValueObject.php b/src/Common/AbstractDataValueObject.php index 82d99c5b..db34ad53 100644 --- a/src/Common/AbstractDataValueObject.php +++ b/src/Common/AbstractDataValueObject.php @@ -46,7 +46,7 @@ protected static function validateFromArrayData(array $data, array $requiredKeys $missingKeys = []; foreach ($requiredKeys as $key) { - if (!isset($data[$key])) { + if (!array_key_exists($key, $data)) { $missingKeys[] = $key; } } From 342c07a78da5ca89a1f7d4444f6a93c05cbae039 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 17:26:35 -0600 Subject: [PATCH 30/36] refacor: removes non-required key validation --- src/Messages/DTO/MessagePart.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Messages/DTO/MessagePart.php b/src/Messages/DTO/MessagePart.php index abd3e4a2..1a95032d 100644 --- a/src/Messages/DTO/MessagePart.php +++ b/src/Messages/DTO/MessagePart.php @@ -260,8 +260,6 @@ public function toArray(): array */ public static function fromArray(array $array): self { - static::validateFromArrayData($array, [self::KEY_TYPE]); - // Check which properties are set to determine how to construct the MessagePart if (isset($array[self::KEY_TEXT])) { return new self($array[self::KEY_TEXT]); From 8ded94df68c4b55c22539ab1242701f988f8e2c2 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 17:29:07 -0600 Subject: [PATCH 31/36] feat: validates that successful oepration has result --- src/Operations/DTO/GenerativeAiOperation.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Operations/DTO/GenerativeAiOperation.php b/src/Operations/DTO/GenerativeAiOperation.php index 01951423..d6efb7b0 100644 --- a/src/Operations/DTO/GenerativeAiOperation.php +++ b/src/Operations/DTO/GenerativeAiOperation.php @@ -175,6 +175,12 @@ public static function fromArray(array $array): self static::validateFromArrayData($array, [self::KEY_ID, self::KEY_STATE]); $state = OperationStateEnum::from($array[self::KEY_STATE]); + + if ($state->isSucceeded()) { + // If the operation has succeeded, it must have a result + static::validateFromArrayData($array, [self::KEY_RESULT]); + } + $result = null; if (isset($array[self::KEY_RESULT])) { $result = GenerativeAiResult::fromArray($array[self::KEY_RESULT]); From 6b48befd00c4d9b4623f022115fa62feb26344ef Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 17:33:46 -0600 Subject: [PATCH 32/36] refactor: removes potentially problematic schema condition --- src/Tools/DTO/FunctionCall.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Tools/DTO/FunctionCall.php b/src/Tools/DTO/FunctionCall.php index 9722c0ec..97c1f4d4 100644 --- a/src/Tools/DTO/FunctionCall.php +++ b/src/Tools/DTO/FunctionCall.php @@ -126,9 +126,6 @@ public static function getJsonSchema(): array [ 'required' => [self::KEY_NAME], ], - [ - 'required' => [self::KEY_ID, self::KEY_NAME], - ], ], ]; } From 3eb52799018f453f2759e527d216aab224cbd079 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 17:35:43 -0600 Subject: [PATCH 33/36] fix: corrects either id or name to be required --- src/Tools/DTO/FunctionResponse.php | 20 +++++++++++++++---- tests/unit/Tools/DTO/FunctionCallTest.php | 5 +---- tests/unit/Tools/DTO/FunctionResponseTest.php | 12 ++++++++--- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Tools/DTO/FunctionResponse.php b/src/Tools/DTO/FunctionResponse.php index e1b6ef48..4f4d4aeb 100644 --- a/src/Tools/DTO/FunctionResponse.php +++ b/src/Tools/DTO/FunctionResponse.php @@ -113,7 +113,14 @@ public static function getJsonSchema(): array 'description' => 'The response data from the function.', ], ], - 'required' => [self::KEY_ID, self::KEY_NAME, self::KEY_RESPONSE], + 'oneOf' => [ + [ + 'required' => [self::KEY_RESPONSE, self::KEY_ID], + ], + [ + 'required' => [self::KEY_RESPONSE, self::KEY_NAME], + ], + ], ]; } @@ -140,11 +147,16 @@ public function toArray(): array */ public static function fromArray(array $array): self { - static::validateFromArrayData($array, [self::KEY_ID, self::KEY_NAME, self::KEY_RESPONSE]); + static::validateFromArrayData($array, [self::KEY_RESPONSE]); + + // Validate that at least one of id or name is provided + if (!array_key_exists(self::KEY_ID, $array) && !array_key_exists(self::KEY_NAME, $array)) { + throw new \InvalidArgumentException('At least one of id or name must be provided.'); + } return new self( - $array[self::KEY_ID], - $array[self::KEY_NAME], + $array[self::KEY_ID] ?? null, + $array[self::KEY_NAME] ?? null, $array[self::KEY_RESPONSE] ); } diff --git a/tests/unit/Tools/DTO/FunctionCallTest.php b/tests/unit/Tools/DTO/FunctionCallTest.php index c0d7dc91..9bfe0b1f 100644 --- a/tests/unit/Tools/DTO/FunctionCallTest.php +++ b/tests/unit/Tools/DTO/FunctionCallTest.php @@ -123,16 +123,13 @@ public function testJsonSchema(): void // Check oneOf for required fields $this->assertArrayHasKey('oneOf', $schema); - $this->assertCount(3, $schema['oneOf']); + $this->assertCount(2, $schema['oneOf']); // First option: only id required $this->assertEquals([FunctionCall::KEY_ID], $schema['oneOf'][0]['required']); // Second option: only name required $this->assertEquals([FunctionCall::KEY_NAME], $schema['oneOf'][1]['required']); - - // Third option: both id and name required - $this->assertEquals([FunctionCall::KEY_ID, FunctionCall::KEY_NAME], $schema['oneOf'][2]['required']); } /** diff --git a/tests/unit/Tools/DTO/FunctionResponseTest.php b/tests/unit/Tools/DTO/FunctionResponseTest.php index 6fd66a87..ad7b3927 100644 --- a/tests/unit/Tools/DTO/FunctionResponseTest.php +++ b/tests/unit/Tools/DTO/FunctionResponseTest.php @@ -122,9 +122,15 @@ public function testJsonSchema(): void $this->assertContains('array', $responseTypes); $this->assertContains('null', $responseTypes); - // Check required fields - $this->assertArrayHasKey('required', $schema); - $this->assertEquals([FunctionResponse::KEY_ID, FunctionResponse::KEY_NAME, FunctionResponse::KEY_RESPONSE], $schema['required']); + // Check oneOf for required fields + $this->assertArrayHasKey('oneOf', $schema); + $this->assertCount(2, $schema['oneOf']); + + // First option: response and id required + $this->assertEquals([FunctionResponse::KEY_RESPONSE, FunctionResponse::KEY_ID], $schema['oneOf'][0]['required']); + + // Second option: response and name required + $this->assertEquals([FunctionResponse::KEY_RESPONSE, FunctionResponse::KEY_NAME], $schema['oneOf'][1]['required']); } /** From 7d53157d26e028ed91ad9545335d4f6719275b50 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 17:41:45 -0600 Subject: [PATCH 34/36] fix: removes unnecessary Tool type validation --- src/Tools/DTO/Tool.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Tools/DTO/Tool.php b/src/Tools/DTO/Tool.php index 1b335c11..b566c9e7 100644 --- a/src/Tools/DTO/Tool.php +++ b/src/Tools/DTO/Tool.php @@ -176,8 +176,6 @@ public function toArray(): array */ public static function fromArray(array $array): self { - static::validateFromArrayData($array, [self::KEY_TYPE]); - // Check which properties are set to determine how to construct the Tool if (isset($array[self::KEY_FUNCTION_DECLARATIONS])) { $declarations = array_map(function (array $declarationData) { From 75a8034d2584859010c723e0cfbe9591cbc4896b Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 20:20:13 -0600 Subject: [PATCH 35/36] chore: marks mimeType required --- src/Files/DTO/File.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files/DTO/File.php b/src/Files/DTO/File.php index 475f7e18..14e3b64c 100644 --- a/src/Files/DTO/File.php +++ b/src/Files/DTO/File.php @@ -21,7 +21,7 @@ * @phpstan-type FileArrayShape array{ * fileType: string, * url?: string, - * mimeType?: string, + * mimeType: string, * base64Data?: string * } * From 19b4692f501e1ece06e0995b7b0319ec18997ef2 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 29 Jul 2025 20:25:32 -0600 Subject: [PATCH 36/36] chore: fixes since tags --- src/Common/Contracts/WithArrayTransformationInterface.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Common/Contracts/WithArrayTransformationInterface.php b/src/Common/Contracts/WithArrayTransformationInterface.php index f867d773..257647f0 100644 --- a/src/Common/Contracts/WithArrayTransformationInterface.php +++ b/src/Common/Contracts/WithArrayTransformationInterface.php @@ -7,7 +7,7 @@ /** * Interface for objects that support array transformation. * - * @since 1.0.0 + * @since n.e.x.t * * @template TArrayShape of array */ @@ -16,7 +16,7 @@ interface WithArrayTransformationInterface /** * Converts the object to an array representation. * - * @since 1.0.0 + * @since n.e.x.t * * @return TArrayShape The array representation. */ @@ -25,7 +25,7 @@ public function toArray(): array; /** * Creates an instance from array data. * - * @since 1.0.0 + * @since n.e.x.t * * @param TArrayShape $array The array data. * @return self The created instance.