1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus MCP SDK package.
7: *
8: * (c) 2025 John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\Mcp\Schema\Elicitation;
15:
16: use Nexus\Mcp\Schema\Arrayable;
17: use Nexus\Mcp\Schema\Elicitation\Builder\PrimitiveSchemaDefinitionBuilder;
18:
19: /**
20: * @implements Arrayable<array{
21: * type: 'object',
22: * properties: array<non-empty-string, array<string, mixed>>,
23: * required?: list<non-empty-string>
24: * }>
25: */
26: final class RequestedSchema implements \JsonSerializable, Arrayable
27: {
28: /**
29: * @var array<non-empty-string, array<string, mixed>>
30: */
31: private array $properties = [];
32:
33: /**
34: * @var list<non-empty-string>
35: */
36: private array $required = [];
37:
38: private function __construct() {}
39:
40: /**
41: * @template T
42: * @template U of array<string, mixed>
43: *
44: * @param PrimitiveSchemaDefinitionBuilder<covariant T, covariant U> ...$properties
45: */
46: public static function fromProperties(PrimitiveSchemaDefinitionBuilder ...$properties): self
47: {
48: $schema = new self();
49:
50: foreach ($properties as $property) {
51: $schema->addProperty($property);
52:
53: if ($property->isRequired()) {
54: $schema->markAsRequired($property->name);
55: }
56: }
57:
58: return $schema;
59: }
60:
61: /**
62: * Adds a property to the schema.
63: *
64: * If a property with the same name already exists, it will not be added again.
65: * This method does not mark the property as required. Use `markAsRequired` for that.
66: *
67: * @template T
68: * @template U of array<string, mixed>
69: *
70: * @param PrimitiveSchemaDefinitionBuilder<covariant T, covariant U> $property
71: */
72: public function addProperty(PrimitiveSchemaDefinitionBuilder $property): self
73: {
74: if (! \array_key_exists($property->name, $this->properties)) {
75: $this->properties[$property->name] = $property->build()->toArray();
76: }
77:
78: return $this;
79: }
80:
81: /**
82: * Marks a property as required in the schema.
83: *
84: * @param non-empty-string $name
85: */
86: public function markAsRequired(string $name): self
87: {
88: if (! \array_key_exists($name, $this->properties)) {
89: throw new \InvalidArgumentException(\sprintf(
90: 'Cannot mark property "%s" as required because it does not exist in the schema.',
91: $name,
92: ));
93: }
94:
95: if (! \in_array($name, $this->required, true)) {
96: $this->required[] = $name;
97: }
98:
99: return $this;
100: }
101:
102: #[\Override]
103: public function toArray(): array
104: {
105: if ([] === $this->properties) {
106: throw new \LogicException('At least one property must be defined to create the elicitation\'s requested schema.');
107: }
108:
109: $schema = [
110: 'type' => 'object',
111: 'properties' => $this->properties,
112: ];
113:
114: if ([] !== $this->required) {
115: $schema['required'] = $this->required;
116: }
117:
118: return $schema;
119: }
120:
121: /**
122: * @return template-type<self, Arrayable, 'T'>
123: */
124: #[\Override]
125: public function jsonSerialize(): array
126: {
127: return $this->toArray();
128: }
129: }
130: