1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus MCP SDK package.
7: *
8: * (c) 2026 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\Core\Schema\Elicitation;
15:
16: use Nexus\Assert\Assert;
17:
18: /**
19: * Schema for a single-line string elicitation field.
20: *
21: * @implements PrimitiveSchemaDefinition<array{
22: * type: 'string',
23: * title?: non-empty-string,
24: * description?: non-empty-string,
25: * minLength?: int<0, max>,
26: * maxLength?: int<0, max>,
27: * format?: 'date'|'date-time'|'email'|'uri',
28: * default?: string,
29: * }>
30: *
31: * @see https://modelcontextprotocol.io/specification/draft/schema#stringschema
32: */
33: final readonly class StringSchema implements PrimitiveSchemaDefinition
34: {
35: public const string TYPE = 'string';
36: public const string FORMAT_DATE = 'date';
37: public const string FORMAT_DATE_TIME = 'date-time';
38: public const string FORMAT_EMAIL = 'email';
39: public const string FORMAT_URI = 'uri';
40:
41: /**
42: * @var null|non-empty-string
43: */
44: public ?string $title;
45:
46: /**
47: * @var null|non-empty-string
48: */
49: public ?string $description;
50:
51: /**
52: * @var null|'date'|'date-time'|'email'|'uri'
53: */
54: public ?string $format;
55:
56: /**
57: * @var null|int<0, max>
58: */
59: public ?int $minLength;
60:
61: /**
62: * @var null|int<0, max>
63: */
64: public ?int $maxLength;
65:
66: public function __construct(
67: ?string $title = null,
68: ?string $description = null,
69: ?int $minLength = null,
70: ?int $maxLength = null,
71: ?string $format = null,
72: public ?string $default = null,
73: ) {
74: Assert::that($title)
75: ->nullOr()
76: ->isNonEmptyString('string schema "title" must be a non-empty string or null.')
77: ;
78: Assert::that($description)
79: ->nullOr()
80: ->isNonEmptyString('string schema "description" must be a non-empty string or null.')
81: ;
82: Assert::that($minLength)
83: ->nullOr()
84: ->isNaturalInt('string schema "minLength" must be a non-negative integer or null.')
85: ;
86: Assert::that($maxLength)
87: ->nullOr()
88: ->isNaturalInt('string schema "maxLength" must be a non-negative integer or null.')
89: ;
90: Assert::that($format)
91: ->nullOr()
92: ->isOneOf(
93: [self::FORMAT_DATE, self::FORMAT_DATE_TIME, self::FORMAT_EMAIL, self::FORMAT_URI],
94: 'string schema "format" must be one of "date", "date-time", "email", "uri".',
95: )
96: ;
97:
98: $this->title = $title;
99: $this->description = $description;
100: $this->format = $format;
101: $this->minLength = $minLength;
102: $this->maxLength = $maxLength;
103: }
104:
105: #[\Override]
106: public static function fromArray(array $data): static
107: {
108: Assert::that($data)->hasOffset('type', 'string schema is missing the required "type" key.');
109: $type = $data['type'];
110: Assert::that($type)->isIdentical(self::TYPE, 'string schema "type" must be {other}, {value} given.');
111:
112: $title = $data['title'] ?? null;
113: Assert::that($title)->nullOr()->isString('string schema "title" must be a string or null, {type} given.');
114:
115: $description = $data['description'] ?? null;
116: Assert::that($description)->nullOr()->isString('string schema "description" must be a string or null, {type} given.');
117:
118: $minLength = $data['minLength'] ?? null;
119: Assert::that($minLength)->nullOr()->isInt('string schema "minLength" must be an int or null, {type} given.');
120:
121: $maxLength = $data['maxLength'] ?? null;
122: Assert::that($maxLength)->nullOr()->isInt('string schema "maxLength" must be an int or null, {type} given.');
123:
124: $format = $data['format'] ?? null;
125: Assert::that($format)->nullOr()->isString('string schema "format" must be a string or null, {type} given.');
126:
127: $default = $data['default'] ?? null;
128: Assert::that($default)->nullOr()->isString('string schema "default" must be a string or null, {type} given.');
129:
130: return new self(
131: title: $title,
132: description: $description,
133: minLength: $minLength,
134: maxLength: $maxLength,
135: format: $format,
136: default: $default,
137: );
138: }
139:
140: #[\Override]
141: public function toArray(): array
142: {
143: $data = ['type' => self::TYPE];
144:
145: if (null !== $this->title) {
146: $data['title'] = $this->title;
147: }
148:
149: if (null !== $this->description) {
150: $data['description'] = $this->description;
151: }
152:
153: if (null !== $this->minLength) {
154: $data['minLength'] = $this->minLength;
155: }
156:
157: if (null !== $this->maxLength) {
158: $data['maxLength'] = $this->maxLength;
159: }
160:
161: if (null !== $this->format) {
162: $data['format'] = $this->format;
163: }
164:
165: if (null !== $this->default) {
166: $data['default'] = $this->default;
167: }
168:
169: return $data;
170: }
171:
172: #[\Override]
173: public function jsonSerialize(): array
174: {
175: return $this->toArray();
176: }
177: }
178: