Skip to content

Commit 2c1beaf

Browse files
refactor: simplify MessageFactory to use single method-based registry
1 parent 8604fe9 commit 2c1beaf

File tree

3 files changed

+34
-106
lines changed

3 files changed

+34
-106
lines changed

src/JsonRpc/MessageFactory.php

Lines changed: 20 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@
3535
final class MessageFactory
3636
{
3737
/**
38-
* Registry of all known notification classes.
38+
* Registry of all known message classes that have methods.
3939
*
40-
* @var array<int, class-string<Notification>>
40+
* @var array<int, class-string<Request|Notification>>
4141
*/
42-
private const REGISTERED_NOTIFICATIONS = [
42+
private const REGISTERED_MESSAGES = [
4343
Schema\Notification\CancelledNotification::class,
4444
Schema\Notification\InitializedNotification::class,
4545
Schema\Notification\LoggingMessageNotification::class,
@@ -49,14 +49,7 @@ final class MessageFactory
4949
Schema\Notification\ResourceUpdatedNotification::class,
5050
Schema\Notification\RootsListChangedNotification::class,
5151
Schema\Notification\ToolListChangedNotification::class,
52-
];
5352

54-
/**
55-
* Registry of all known request classes.
56-
*
57-
* @var array<int, class-string<Request>>
58-
*/
59-
private const REGISTERED_REQUESTS = [
6053
Schema\Request\CallToolRequest::class,
6154
Schema\Request\CompletionCompleteRequest::class,
6255
Schema\Request\CreateSamplingMessageRequest::class,
@@ -75,32 +68,24 @@ final class MessageFactory
7568
];
7669

7770
/**
78-
* @param array<int, class-string<Notification>> $registeredNotifications
79-
* @param array<int, class-string<Request>> $registeredRequests
71+
* @param array<int, class-string<Request|Notification>> $registeredMessages
8072
*/
8173
public function __construct(
82-
private readonly array $registeredNotifications,
83-
private readonly array $registeredRequests,
74+
private readonly array $registeredMessages,
8475
) {
85-
foreach ($this->registeredNotifications as $notification) {
86-
if (!is_subclass_of($notification, Notification::class)) {
87-
throw new InvalidArgumentException(\sprintf('Notification classes must extend %s.', Notification::class));
88-
}
89-
}
90-
91-
foreach ($this->registeredRequests as $request) {
92-
if (!is_subclass_of($request, Request::class)) {
93-
throw new InvalidArgumentException(\sprintf('Request classes must extend %s.', Request::class));
76+
foreach ($this->registeredMessages as $messageClass) {
77+
if (!is_subclass_of($messageClass, Request::class) && !is_subclass_of($messageClass, Notification::class)) {
78+
throw new InvalidArgumentException(\sprintf('Message classes must extend %s or %s.', Request::class, Notification::class));
9479
}
9580
}
9681
}
9782

9883
/**
99-
* Creates a new Factory instance with all the protocol's default notifications and requests.
84+
* Creates a new Factory instance with all the protocol's default messages.
10085
*/
10186
public static function make(): self
10287
{
103-
return new self(self::REGISTERED_NOTIFICATIONS, self::REGISTERED_REQUESTS);
88+
return new self(self::REGISTERED_MESSAGES);
10489
}
10590

10691
/**
@@ -142,10 +127,6 @@ public function create(string $input): array
142127
*/
143128
private function createMessage(array $data): MessageInterface
144129
{
145-
if (!isset($data['jsonrpc']) || MessageInterface::JSONRPC_VERSION !== $data['jsonrpc']) {
146-
throw new InvalidInputMessageException('Invalid or missing "jsonrpc" version.');
147-
}
148-
149130
try {
150131
if (isset($data['error'])) {
151132
return Error::fromArray($data);
@@ -159,81 +140,29 @@ private function createMessage(array $data): MessageInterface
159140
throw new InvalidInputMessageException('Invalid JSON-RPC message: missing "method", "result", or "error" field.');
160141
}
161142

162-
return isset($data['id']) ? $this->createRequest($data) : $this->createNotification($data);
143+
$messageClass = $this->findMessageClassByMethod($data['method']);
144+
145+
return $messageClass::fromArray($data);
163146
} catch (InvalidArgumentException $e) {
164147
throw new InvalidInputMessageException($e->getMessage(), 0, $e);
165148
}
166149
}
167150

168151
/**
169-
* Creates a Request object by looking up the appropriate class by method name.
170-
*
171-
* @param array<string, mixed> $data
172-
*
173-
* @throws InvalidInputMessageException
174-
*/
175-
private function createRequest(array $data): Request
176-
{
177-
if (!\is_string($data['method'])) {
178-
throw new InvalidInputMessageException('Request "method" must be a string.');
179-
}
180-
181-
$messageClass = $this->findRequestClassByMethod($data['method']);
182-
183-
return $messageClass::fromArray($data);
184-
}
185-
186-
/**
187-
* Creates a Notification object by looking up the appropriate class by method name.
188-
*
189-
* @param array<string, mixed> $data
190-
*
191-
* @throws InvalidInputMessageException
192-
*/
193-
private function createNotification(array $data): Notification
194-
{
195-
if (!\is_string($data['method'])) {
196-
throw new InvalidInputMessageException('Notification "method" must be a string.');
197-
}
198-
199-
$messageClass = $this->findNotificationClassByMethod($data['method']);
200-
201-
return $messageClass::fromArray($data);
202-
}
203-
204-
/**
205-
* Finds the registered request class for a given method name.
206-
*
207-
* @return class-string<Request>
208-
*
209-
* @throws InvalidInputMessageException
210-
*/
211-
private function findRequestClassByMethod(string $method): string
212-
{
213-
foreach ($this->registeredRequests as $requestClass) {
214-
if ($requestClass::getMethod() === $method) {
215-
return $requestClass;
216-
}
217-
}
218-
219-
throw new InvalidInputMessageException(\sprintf('Unknown request method "%s".', $method));
220-
}
221-
222-
/**
223-
* Finds the registered notification class for a given method name.
152+
* Finds the registered message class for a given method name.
224153
*
225-
* @return class-string<Notification>
154+
* @return class-string<Request|Notification>
226155
*
227156
* @throws InvalidInputMessageException
228157
*/
229-
private function findNotificationClassByMethod(string $method): string
158+
private function findMessageClassByMethod(string $method): string
230159
{
231-
foreach ($this->registeredNotifications as $notificationClass) {
232-
if ($notificationClass::getMethod() === $method) {
233-
return $notificationClass;
160+
foreach ($this->registeredMessages as $messageClass) {
161+
if ($messageClass::getMethod() === $method) {
162+
return $messageClass;
234163
}
235164
}
236165

237-
throw new InvalidInputMessageException(\sprintf('Unknown notification method "%s".', $method));
166+
throw new InvalidInputMessageException(\sprintf('Unknown method "%s".', $method));
238167
}
239168
}

tests/Unit/JsonRpc/MessageFactoryTest.php

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,12 @@ final class MessageFactoryTest extends TestCase
2727

2828
protected function setUp(): void
2929
{
30-
$this->factory = new MessageFactory(
31-
[
32-
CancelledNotification::class,
33-
InitializedNotification::class,
34-
],
35-
[
36-
GetPromptRequest::class,
37-
PingRequest::class,
38-
]
39-
);
30+
$this->factory = new MessageFactory([
31+
CancelledNotification::class,
32+
InitializedNotification::class,
33+
GetPromptRequest::class,
34+
PingRequest::class,
35+
]);
4036
}
4137

4238
public function testCreateRequestWithIntegerId(): void
@@ -250,7 +246,7 @@ public function testUnknownMethod(): void
250246

251247
$this->assertCount(1, $results);
252248
$this->assertInstanceOf(InvalidInputMessageException::class, $results[0]);
253-
$this->assertStringContainsString('Unknown request method', $results[0]->getMessage());
249+
$this->assertStringContainsString('Unknown method', $results[0]->getMessage());
254250
}
255251

256252
public function testUnknownNotificationMethod(): void
@@ -261,18 +257,20 @@ public function testUnknownNotificationMethod(): void
261257

262258
$this->assertCount(1, $results);
263259
$this->assertInstanceOf(InvalidInputMessageException::class, $results[0]);
264-
$this->assertStringContainsString('Unknown notification method', $results[0]->getMessage());
260+
$this->assertStringContainsString('Unknown method', $results[0]->getMessage());
265261
}
266262

267-
public function testResponseMissingId(): void
263+
public function testNotificationMethodUsedAsRequest(): void
268264
{
269-
$json = '{"jsonrpc": "2.0", "result": {"status": "ok"}}';
265+
// When a notification method is used with an id, it should still create the notification
266+
// The fromArray validation will handle any issues
267+
$json = '{"jsonrpc": "2.0", "method": "notifications/initialized", "id": 1}';
270268

271269
$results = $this->factory->create($json);
272270

273271
$this->assertCount(1, $results);
272+
// The notification class will reject the id in fromArray validation
274273
$this->assertInstanceOf(InvalidInputMessageException::class, $results[0]);
275-
$this->assertStringContainsString('id', $results[0]->getMessage());
276274
}
277275

278276
public function testErrorMissingId(): void

tests/Unit/Server/ProtocolTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ final class ProtocolTest extends TestCase
3030
{
3131
private MockObject&SessionFactoryInterface $sessionFactory;
3232
private MockObject&SessionStoreInterface $sessionStore;
33+
/** @var MockObject&TransportInterface<mixed> */
3334
private MockObject&TransportInterface $transport;
3435

3536
protected function setUp(): void

0 commit comments

Comments
 (0)