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: use Nexus\Mcp\Core\Schema\Arrayable;
18:
19: /**
20: * Schema for single-selection enumeration with display titles for each option.
21: *
22: * @implements SingleSelectEnumSchema<array{
23: * type: 'string',
24: * oneOf: list<template-type<EnumOption, Arrayable, 'T'>>,
25: * title?: non-empty-string,
26: * description?: non-empty-string,
27: * default?: string,
28: * }>
29: *
30: * @see https://modelcontextprotocol.io/specification/2026-07-28/schema#titledsingleselectenumschema
31: */
32: final readonly class TitledSingleSelectEnumSchema implements SingleSelectEnumSchema
33: {
34: public const string TYPE = 'string';
35:
36: /**
37: * @param list<EnumOption> $oneOf
38: * @param null|non-empty-string $title
39: * @param null|non-empty-string $description
40: */
41: public function __construct(
42: public array $oneOf,
43: public ?string $title = null,
44: public ?string $description = null,
45: public ?string $default = null,
46: ) {
47: Assert::that($oneOf)
48: ->isList('titled single select enum schema "oneOf" must be a list, non-list array given.')
49: ->values()->isInstanceOf(EnumOption::class)
50: ;
51: Assert::that($title)->nullOr()->isNonEmptyString('titled single select enum schema "title" must be a non-empty string or null.');
52: Assert::that($description)->nullOr()->isNonEmptyString('titled single select enum schema "description" must be a non-empty string or null.');
53: }
54:
55: #[\Override]
56: public static function fromArray(array $data): static
57: {
58: Assert::that($data)->hasOffset('type', 'titled single select enum schema is missing the required "type" key.');
59: $type = $data['type'];
60: Assert::that($type)->isIdentical(self::TYPE, 'titled single select enum schema "type" must be {other}, {value} given.');
61:
62: Assert::that($data)->hasOffset('oneOf', 'titled single select enum schema is missing the required "oneOf" key.');
63: Assert::that($data['oneOf'])
64: ->isList('titled single select enum schema "oneOf" must be a list, non-list array given.')
65: ->values()
66: ->isArray('each titled single select enum schema "oneOf" must be an object, {type} given.')
67: ->isMap('each titled single select enum schema "oneOf" must be a string-keyed object.')
68: ;
69: $oneOf = array_map(EnumOption::fromArray(...), $data['oneOf']);
70:
71: $title = $data['title'] ?? null;
72: Assert::that($title)->nullOr()->isNonEmptyString('titled single select enum schema "title" must be a non-empty string or null, {type} given.');
73:
74: $description = $data['description'] ?? null;
75: Assert::that($description)->nullOr()->isNonEmptyString('titled single select enum schema "description" must be a non-empty string or null, {type} given.');
76:
77: $default = $data['default'] ?? null;
78: Assert::that($default)->nullOr()->isString('titled single select enum schema "default" must be a string or null, {type} given.');
79:
80: return new self(oneOf: $oneOf, title: $title, description: $description, default: $default);
81: }
82:
83: #[\Override]
84: public function toArray(): array
85: {
86: $data = [
87: 'type' => self::TYPE,
88: 'oneOf' => array_map(static fn(EnumOption $o): array => $o->toArray(), $this->oneOf),
89: ];
90:
91: if (null !== $this->title) {
92: $data['title'] = $this->title;
93: }
94:
95: if (null !== $this->description) {
96: $data['description'] = $this->description;
97: }
98:
99: if (null !== $this->default) {
100: $data['default'] = $this->default;
101: }
102:
103: return $data;
104: }
105:
106: #[\Override]
107: public function jsonSerialize(): array
108: {
109: return $this->toArray();
110: }
111: }
112: