Lines 98.14% 53 / 54
Methods 80.00% 4 / 5
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 createForWorkerBootstrap 100.00% 1 / 1 100.00% 1 / 1 1
 createIpcWireMapper 100.00% 22 / 22 100.00% 1 / 1 1
 encodeMessage 88.88% 8 / 9 0.00% 0 / 1 3.01
 decodeMessage 100.00% 21 / 21 100.00% 1 / 1 12
25final readonly class IpcCodec
26{
27    public const int PROTOCOL_VERSION = 29;
28
29    private const string UNKNOWN_TYPE = '?';
30
31    public function __construct(
32        private WireMapper $ipcWireMapper,
33        private IpcMessageCatalog $messageCatalog,
34    ) {}
35
36    /** @throws TransportException */
37    public static function createForWorkerBootstrap(): self
38    {
39        return new self(self::createIpcWireMapper(), IpcMessageCatalog::scanMessageDirectory());
40    }
41
42    public static function createIpcWireMapper(): WireMapper
43    {
44        return new WireMapper(new ClassShapeReader(ValueConverterCollection::keyedByHandledClass([
45            new EpochInstantConverter(),
46            new DurationConverter(),
47            new SelectorConverter(),
48            new MqttMessageConverter(),
49            new TriggerSpecConverter(),
50            new ExceptionDetailsConverter(),
51            new ExposedStateConverter(),
52            new TimeOfDayConverter(),
53            new DateTimeImmutableConverter(),
54            StringIdentifierConverter::createForEntityIds(),
55            StringIdentifierConverter::createForAppIds(),
56            StringIdentifierConverter::createForAreaIds(),
57            StringIdentifierConverter::createForFloorIds(),
58            StringIdentifierConverter::createForLabelIds(),
59            StringIdentifierConverter::createForDeviceIds(),
60            new StringIdentifierConverter(SubscriptionId::class, 'a subscription id', SubscriptionId::fromString(...)),
61            new StringIdentifierConverter(CorrelationId::class, 'a correlation id', CorrelationId::fromString(...)),
62            new StringIdentifierConverter(ExposedEntityKey::class, 'an exposed entity key', ExposedEntityKey::tryFromString(...)),
63            new WorkerIdConverter(),
64            new StringIdentifierConverter(ResourceScope::class, 'an app id or "@shared"', ResourceScope::tryFromWireValue(...)),
65        ])));
66    }
67
68    /** @throws TransportException */
69    public function encodeMessage(object $message): string
70    {
71        $tag = $this->messageCatalog->findTagForClass($message::class) ?? throw TransportException::createForReason(
72            TransportError::MessageTypeMissing,
73            messageClass: $message::class,
74        );
75
76        try {
77            return '{"t":"' . $tag . '","m":' . $this->ipcWireMapper->encodeObject($message) . '}';
78        } catch (TransportException $e) {
79            throw $e;
80        } catch (JsonException|StewartException $e) {
81            throw TransportException::createForReason(TransportError::Unencodable, $e, messageClass: $message::class);
82        }
83    }
84
85    /** @throws TransportException */
86    public function decodeMessage(string $frame): object
87    {
88        try {
89            $data = JsonDecoder::decodeJson($frame);
90        } catch (JsonException $e) {
91            throw TransportException::createForReason(TransportError::UndecodableFrame, $e, messageType: self::UNKNOWN_TYPE, reason: $e->getMessage());
92        }
93
94        $tag = \is_array($data) && \is_string($data['t'] ?? null) ? $data['t'] : self::UNKNOWN_TYPE;
95        $body = \is_array($data) ? ($data['m'] ?? null) : null;
96        $class = $this->messageCatalog->findClassForTag($tag);
97
98        if ($class === null || !\is_array($body)) {
99            throw TransportException::createForReason(
100                TransportError::UndecodableFrame,
101                messageType: $tag,
102                reason: $class === null ? 'unknown message type' : 'the frame has no message body',
103            );
104        }
105
106        if ($class === Bootstrap::class && ($body['protocol'] ?? null) !== self::PROTOCOL_VERSION) {
107            throw TransportException::createForReason(
108                TransportError::ProtocolMismatch,
109                expected: self::PROTOCOL_VERSION,
110                actual: \is_int($body['protocol'] ?? null) ? $body['protocol'] : 0,
111            );
112        }
113
114        try {
115            return $this->ipcWireMapper->decodeObject($class, $body);
116        } catch (Throwable $e) {
117            throw TransportException::createForReason(TransportError::UndecodableFrame, $e, messageType: $tag, reason: $e->getMessage());
118        }
119    }
120}