Lines 84.61% 22 / 26
Functions and Methods 66.66% 4 / 6
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
FrameCodec 84.61% 22 / 26 66.66% 4 / 6 12.52 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 createControlWireMapper 100.00% 1 / 1 100.00% 1 / 1 1
 encodeFrame 50.00% 3 / 6 0.00% 0 / 1 4.12
 decodeClientFrame 100.00% 3 / 3 100.00% 1 / 1 1
 decodeServerFrame 100.00% 3 / 3 100.00% 1 / 1 1
 decodeFrameOfType 91.66% 11 / 12 0.00% 0 / 1 5.01
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Runtime\Control\Protocol\Codec;
6
7use JsonException;
8use Stewart\Contracts\Exception\StewartException;
9use Stewart\Runtime\Control\Protocol\Frame\Bye;
10use Stewart\Runtime\Control\Protocol\Frame\ClientFrame;
11use Stewart\Runtime\Control\Protocol\Frame\CommandResult;
12use Stewart\Runtime\Control\Protocol\Frame\Hello;
13use Stewart\Runtime\Control\Protocol\Frame\PauseAppRequest;
14use Stewart\Runtime\Control\Protocol\Frame\Rejected;
15use Stewart\Runtime\Control\Protocol\Frame\RequestFailed;
16use Stewart\Runtime\Control\Protocol\Frame\ResetAppRequest;
17use Stewart\Runtime\Control\Protocol\Frame\RestartWorkerRequest;
18use Stewart\Runtime\Control\Protocol\Frame\ResumeAppRequest;
19use Stewart\Runtime\Control\Protocol\Frame\ServerFrame;
20use Stewart\Runtime\Control\Protocol\Frame\SnapshotFrame;
21use Stewart\Runtime\Control\Protocol\Frame\SnapshotRequest;
22use Stewart\Runtime\Control\Protocol\Frame\Welcome;
23use Stewart\Runtime\Exception\ControlError;
24use Stewart\Runtime\Exception\ControlException;
25use Stewart\Runtime\Json\ClassShapeReader;
26use Stewart\Runtime\Json\Collection\ValueConverterCollection;
27use Stewart\Runtime\Json\DurationConverter;
28use Stewart\Runtime\Json\IsoInstantConverter;
29use Stewart\Runtime\Json\WireMapper;
30use Stewart\Support\Exception\JsonShapeException;
31use Stewart\Support\Json\JsonDecoder;
32use Stewart\Support\Json\JsonEncoder;
33use Stewart\Support\Json\JsonShape;
34use Throwable;
35
36final readonly class FrameCodec
37{
38    private const int DEPTH = 32;
39
40    private const array CLIENT_FRAMES = [
41        'hello' => Hello::class,
42        'snapshot_request' => SnapshotRequest::class,
43        'pause_app' => PauseAppRequest::class,
44        'resume_app' => ResumeAppRequest::class,
45        'reset_app' => ResetAppRequest::class,
46        'restart_worker' => RestartWorkerRequest::class,
47    ];
48
49    private const array SERVER_FRAMES = [
50        'welcome' => Welcome::class,
51        'snapshot' => SnapshotFrame::class,
52        'rejected' => Rejected::class,
53        'command_result' => CommandResult::class,
54        'request_failed' => RequestFailed::class,
55        'bye' => Bye::class,
56    ];
57
58    public function __construct(private WireMapper $controlWireMapper) {}
59
60    public static function createControlWireMapper(): WireMapper
61    {
62        return new WireMapper(new ClassShapeReader(ValueConverterCollection::keyedByHandledClass([new IsoInstantConverter(), new DurationConverter()])));
63    }
64
65    /** @throws ControlException */
66    public function encodeFrame(ClientFrame|ServerFrame $frame): string
67    {
68        $type = array_search($frame::class, self::CLIENT_FRAMES + self::SERVER_FRAMES, true);
69
70        if ($type === false) {
71            throw ControlException::createForReason(ControlError::FrameClassUnknown, frameClass: $frame::class);
72        }
73
74        try {
75            return JsonEncoder::encodeToJson(['type' => $type, ...(array) $this->controlWireMapper->normalizeObject($frame)], self::DEPTH) . "\n";
76        } catch (JsonException|StewartException $e) {
77            throw ControlException::createForReason(ControlError::FrameUnencodable, $e, frameClass: $frame::class);
78        }
79    }
80
81    /** @throws ControlException|StewartException */
82    public function decodeClientFrame(string $line): ClientFrame
83    {
84        $frame = $this->decodeFrameOfType($line, self::CLIENT_FRAMES, 'a client frame type');
85        \assert($frame instanceof ClientFrame);
86
87        return $frame;
88    }
89
90    /** @throws ControlException|StewartException */
91    public function decodeServerFrame(string $line): ServerFrame
92    {
93        $frame = $this->decodeFrameOfType($line, self::SERVER_FRAMES, 'a server frame type');
94        \assert($frame instanceof ServerFrame);
95
96        return $frame;
97    }
98
99    /**
100     * @param array<string, class-string> $frames
101     * @throws ControlException|StewartException
102     */
103    private function decodeFrameOfType(string $line, array $frames, string $expected): object
104    {
105        try {
106            $data = JsonDecoder::decodeJson($line, self::DEPTH);
107        } catch (JsonException $e) {
108            throw ControlException::createForReason(ControlError::FrameNotJson, $e);
109        }
110
111        if (!\is_array($data)) {
112            throw ControlException::createForReason(ControlError::FrameNotAnObject, actualType: get_debug_type($data));
113        }
114
115        $type = JsonShape::requireString($data, 'type');
116        $class = $frames[$type] ?? throw JsonShapeException::unexpectedValue('type', $expected, $type);
117
118        try {
119            return $this->controlWireMapper->decodeObject($class, $data);
120        } catch (StewartException $e) {
121            throw $e;
122        } catch (Throwable $e) {
123            throw ControlException::createForReason(ControlError::FrameUndecodable, $e, frameType: $type);
124        }
125    }
126}