Lines 84.61% 22 / 26
Methods 66.66% 4 / 6
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __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
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}