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 | ||
| 25 | final 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 | } |