Lines 92.10% 35 / 38
Methods 80.00% 8 / 10
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 encodeValue 100.00% 6 / 6 100.00% 1 / 1 4
 decodeInt 100.00% 2 / 2 100.00% 1 / 1 2
 decodeFloat 100.00% 4 / 4 100.00% 1 / 1 4
 decodeString 100.00% 2 / 2 100.00% 1 / 1 2
 decodeBool 0.00% 0 / 2 0.00% 0 / 1 6
 decodeObject 87.50% 7 / 8 0.00% 0 / 1 4.03
 decodeJson 100.00% 3 / 3 100.00% 1 / 1 2
 encodeJson 100.00% 3 / 3 100.00% 1 / 1 2
 typeMismatch 100.00% 1 / 1 100.00% 1 / 1 1
 describeJsonType 100.00% 7 / 7 100.00% 1 / 1 9
16final readonly class StoreValueCodec
17{
18    /** @throws StoreException */
19    public function encodeValue(string $key, bool|int|float|string|Storable $value): string
20    {
21        if (!$value instanceof Storable) {
22            return $this->encodeJson($key, $value);
23        }
24
25        $data = $value->toStorage();
26
27        if ($data !== [] && array_is_list($data)) {
28            throw StoreException::createForReason(StoreError::ValueNotAMap, key: $key, class: $value::class);
29        }
30
31        return $this->encodeJson($key, (object) $data);
32    }
33
34    /** @throws StoreException */
35    public function decodeInt(string $key, string $raw): int
36    {
37        $value = $this->decodeJson($key, $raw);
38
39        return \is_int($value) ? $value : throw $this->typeMismatch($key, 'an integer', $value);
40    }
41
42    /** @throws StoreException */
43    public function decodeFloat(string $key, string $raw): float
44    {
45        $value = $this->decodeJson($key, $raw);
46
47        return match (true) {
48            \is_float($value) => $value,
49            \is_int($value) => (float) $value,
50            default => throw $this->typeMismatch($key, 'a number', $value),
51        };
52    }
53
54    /** @throws StoreException */
55    public function decodeString(string $key, string $raw): string
56    {
57        $value = $this->decodeJson($key, $raw);
58
59        return \is_string($value) ? $value : throw $this->typeMismatch($key, 'a string', $value);
60    }
61
62    /** @throws StoreException */
63    public function decodeBool(string $key, string $raw): bool
64    {
65        $value = $this->decodeJson($key, $raw);
66
67        return \is_bool($value) ? $value : throw $this->typeMismatch($key, 'a boolean', $value);
68    }
69
70    /**
71     * @template T of Storable
72     *
73     * @param class-string<T> $class
74     * @return T
75     * @throws StoreException
76     */
77    public function decodeObject(string $key, string $raw, string $class): Storable
78    {
79        // json_decode returns an array for both {} and []; objects are always written as {}.
80        if (!str_starts_with(ltrim($raw), '{')) {
81            throw $this->typeMismatch($key, 'an object', $this->decodeJson($key, $raw));
82        }
83
84        $data = $this->decodeJson($key, $raw);
85
86        if (!\is_array($data)) {
87            throw $this->typeMismatch($key, 'an object', $data);
88        }
89
90        try {
91            return $class::fromStorage(JsonShape::treatKeysAsStrings($data));
92        } catch (Throwable $e) {
93            throw StoreException::createForReason(StoreError::ValueRejected, $e, key: $key, class: $class);
94        }
95    }
96
97    /** @throws StoreException */
98    private function decodeJson(string $key, string $raw): mixed
99    {
100        try {
101            return JsonDecoder::decodeJson($raw);
102        } catch (JsonException $e) {
103            throw StoreException::createForReason(StoreError::ValueMalformed, $e, key: $key);
104        }
105    }
106
107    /** @throws StoreException */
108    private function encodeJson(string $key, mixed $value): string
109    {
110        try {
111            return JsonEncoder::encodeToJson($value);
112        } catch (JsonException $e) {
113            throw StoreException::createForReason(StoreError::ValueNotEncodable, $e, key: $key);
114        }
115    }
116
117    private function typeMismatch(string $key, string $expected, mixed $actual): StoreException
118    {
119        return StoreException::createForReason(StoreError::TypeMismatch, key: $key, expected: $expected, actual: $this->describeJsonType($actual));
120    }
121
122    private function describeJsonType(mixed $value): string
123    {
124        return match (true) {
125            $value === null => 'null',
126            \is_bool($value) => 'a boolean',
127            \is_int($value) => 'an integer',
128            \is_float($value) => 'a float',
129            \is_string($value) => 'a string',
130            \is_array($value) && array_is_list($value) => 'a list',
131            default => 'an object',
132        };
133    }
134}