Lines
82.05%
64 / 78
Functions and Methods
80.95%
17 / 21
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ConfigSection | 82.05% | 64 / 78 | 80.95% | 17 / 21 | 55.19 | 0.00% | 0 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| forRoot | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| readSection | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| findSection | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | |||||
| mapSubsections | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 3 | |||||
| readInt | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | |||||
| readBool | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | |||||
| readString | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| findString | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 3 | |||||
| findInt | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | |||||
| readStringList | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| readStringMap | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 3 | |||||
| readMap | 85.71% | 6 / 7 | 0.00% | 0 / 1 | 3.03 | |||||
| readDuration | 62.50% | 10 / 16 | 0.00% | 0 / 1 | 4.84 | |||||
| findSwitchableValue | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 4 | |||||
| readOptionalDuration | 40.00% | 4 / 10 | 0.00% | 0 / 1 | 4.94 | |||||
| readBackoff | 100.00% | 12 / 12 | 100.00% | 1 / 1 | 2 | |||||
| readParsedValue | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | |||||
| createKeyInvalidFailure | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | |||||
| createSchemaMismatchFailure | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| buildKeyPath | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 2 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Stewart\Runtime\Config; | |
| 6 | ||
| 7 | use Closure; | |
| 8 | use LogicException; | |
| 9 | use SensitiveParameter; | |
| 10 | use Stewart\Contracts\Exception\StewartException; | |
| 11 | use Stewart\Contracts\Time\Duration; | |
| 12 | use Stewart\Runtime\Exception\ConfigurationError; | |
| 13 | use Stewart\Runtime\Exception\ConfigurationException; | |
| 14 | ||
| 15 | final readonly class ConfigSection | |
| 16 | { | |
| 17 | /** @param array<array-key, mixed> $values */ | |
| 18 | private function __construct( | |
| 19 | #[SensitiveParameter] | |
| 20 | private array $values, | |
| 21 | private string $path, | |
| 22 | ) {} | |
| 23 | ||
| 24 | /** @param array<array-key, mixed> $values */ | |
| 25 | public static function forRoot(#[SensitiveParameter] array $values): self | |
| 26 | { | |
| 27 | return new self($values, ''); | |
| 28 | } | |
| 29 | ||
| 30 | public function readSection(string $key): self | |
| 31 | { | |
| 32 | return $this->findSection($key) ?? throw $this->createSchemaMismatchFailure($key); | |
| 33 | } | |
| 34 | ||
| 35 | public function findSection(string $key): ?self | |
| 36 | { | |
| 37 | $value = $this->values[$key] ?? null; | |
| 38 | ||
| 39 | return \is_array($value) ? new self($value, $this->buildKeyPath($key)) : null; | |
| 40 | } | |
| 41 | ||
| 42 | /** | |
| 43 | * @template T | |
| 44 | * @param Closure(self, string): T $read | |
| 45 | * @return list<T> | |
| 46 | */ | |
| 47 | public function mapSubsections(Closure $read): array | |
| 48 | { | |
| 49 | $mapped = []; | |
| 50 | ||
| 51 | foreach ($this->values as $key => $value) { | |
| 52 | if (\is_array($value)) { | |
| 53 | $mapped[] = $read(new self($value, $this->buildKeyPath((string) $key)), (string) $key); | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | return $mapped; | |
| 58 | } | |
| 59 | ||
| 60 | public function readInt(string $key): int | |
| 61 | { | |
| 62 | $value = $this->values[$key] ?? null; | |
| 63 | ||
| 64 | return \is_int($value) ? $value : throw $this->createSchemaMismatchFailure($key); | |
| 65 | } | |
| 66 | ||
| 67 | public function readBool(string $key): bool | |
| 68 | { | |
| 69 | $value = $this->values[$key] ?? null; | |
| 70 | ||
| 71 | return \is_bool($value) ? $value : throw $this->createSchemaMismatchFailure($key); | |
| 72 | } | |
| 73 | ||
| 74 | public function readString(string $key): string | |
| 75 | { | |
| 76 | return $this->findString($key) ?? throw $this->createSchemaMismatchFailure($key); | |
| 77 | } | |
| 78 | ||
| 79 | public function findString(string $key): ?string | |
| 80 | { | |
| 81 | $value = $this->values[$key] ?? null; | |
| 82 | ||
| 83 | return \is_scalar($value) && !\is_bool($value) ? (string) $value : null; | |
| 84 | } | |
| 85 | ||
| 86 | public function findInt(string $key): ?int | |
| 87 | { | |
| 88 | $value = $this->values[$key] ?? null; | |
| 89 | ||
| 90 | return \is_int($value) ? $value : null; | |
| 91 | } | |
| 92 | ||
| 93 | /** | |
| 94 | * @return list<string> | |
| 95 | * @throws ConfigurationException | |
| 96 | */ | |
| 97 | public function readStringList(string $key): array | |
| 98 | { | |
| 99 | return array_values($this->readStringMap($key)); | |
| 100 | } | |
| 101 | ||
| 102 | /** | |
| 103 | * @return array<string, string> | |
| 104 | * @throws ConfigurationException | |
| 105 | */ | |
| 106 | public function readStringMap(string $key): array | |
| 107 | { | |
| 108 | $strings = []; | |
| 109 | ||
| 110 | foreach ($this->readMap($key) as $name => $value) { | |
| 111 | $strings[$name] = \is_string($value) ? $value : throw $this->createKeyInvalidFailure($key . '.' . $name, 'a string'); | |
| 112 | } | |
| 113 | ||
| 114 | return $strings; | |
| 115 | } | |
| 116 | ||
| 117 | /** @return array<string, mixed> */ | |
| 118 | public function readMap(string $key): array | |
| 119 | { | |
| 120 | $value = $this->values[$key] ?? []; | |
| 121 | ||
| 122 | if (!\is_array($value)) { | |
| 123 | throw $this->createSchemaMismatchFailure($key); | |
| 124 | } | |
| 125 | ||
| 126 | $map = []; | |
| 127 | ||
| 128 | foreach ($value as $name => $item) { | |
| 129 | $map[(string) $name] = $item; | |
| 130 | } | |
| 131 | ||
| 132 | return $map; | |
| 133 | } | |
| 134 | ||
| 135 | /** @throws ConfigurationException */ | |
| 136 | public function readDuration(string $key, ?Duration $atLeast = null, ?Duration $atMost = null): Duration | |
| 137 | { | |
| 138 | $duration = $this->readParsedValue($key, Duration::parse(...)); | |
| 139 | ||
| 140 | if ($atLeast?->isLongerThan($duration) === true) { | |
| 141 | throw ConfigurationException::createForReason( | |
| 142 | ConfigurationError::DurationTooShort, | |
| 143 | path: $this->buildKeyPath($key), | |
| 144 | value: (string) $duration, | |
| 145 | minimum: (string) $atLeast, | |
| 146 | ); | |
| 147 | } | |
| 148 | ||
| 149 | if ($atMost !== null && $duration->isLongerThan($atMost)) { | |
| 150 | throw ConfigurationException::createForReason( | |
| 151 | ConfigurationError::DurationTooLong, | |
| 152 | path: $this->buildKeyPath($key), | |
| 153 | value: (string) $duration, | |
| 154 | maximum: (string) $atMost, | |
| 155 | ); | |
| 156 | } | |
| 157 | ||
| 158 | return $duration; | |
| 159 | } | |
| 160 | ||
| 161 | /** | |
| 162 | * @template T | |
| 163 | * @param Closure(string): T $parse | |
| 164 | * @return T|null | |
| 165 | * @throws ConfigurationException | |
| 166 | */ | |
| 167 | public function findSwitchableValue(string $key, Closure $parse): mixed | |
| 168 | { | |
| 169 | $raw = $this->findString($key); | |
| 170 | ||
| 171 | return $raw === null || $raw === '' || StewartConfigSchema::isOff($raw) ? null : $this->readParsedValue($key, $parse); | |
| 172 | } | |
| 173 | ||
| 174 | /** @throws ConfigurationException */ | |
| 175 | public function readOptionalDuration(string $key, ?Duration $atLeast = null): OptionalDuration | |
| 176 | { | |
| 177 | $optional = $this->readParsedValue($key, OptionalDuration::parse(...)); | |
| 178 | $duration = $optional->findDuration(); | |
| 179 | ||
| 180 | if ($duration !== null && $atLeast?->isLongerThan($duration) === true) { | |
| 181 | throw ConfigurationException::createForReason( | |
| 182 | ConfigurationError::DurationTooShort, | |
| 183 | path: $this->buildKeyPath($key), | |
| 184 | value: (string) $duration, | |
| 185 | minimum: (string) $atLeast, | |
| 186 | ); | |
| 187 | } | |
| 188 | ||
| 189 | return $optional; | |
| 190 | } | |
| 191 | ||
| 192 | /** @throws ConfigurationException */ | |
| 193 | public function readBackoff(string $keyPrefix): BackoffPolicy | |
| 194 | { | |
| 195 | $oneMillisecond = Duration::milliseconds(1); | |
| 196 | $initialDelay = $this->readDuration($keyPrefix . 'initial_delay', $oneMillisecond); | |
| 197 | $maxDelay = $this->readDuration($keyPrefix . 'max_delay', $oneMillisecond); | |
| 198 | ||
| 199 | if ($initialDelay->isLongerThan($maxDelay)) { | |
| 200 | throw ConfigurationException::createForReason( | |
| 201 | ConfigurationError::MaxDelayBelowInitialDelay, | |
| 202 | path: $this->buildKeyPath($keyPrefix . 'max_delay'), | |
| 203 | value: (string) $maxDelay, | |
| 204 | initialDelayPath: $this->buildKeyPath($keyPrefix . 'initial_delay'), | |
| 205 | initialDelay: (string) $initialDelay, | |
| 206 | ); | |
| 207 | } | |
| 208 | ||
| 209 | return new BackoffPolicy($initialDelay, $maxDelay); | |
| 210 | } | |
| 211 | ||
| 212 | /** | |
| 213 | * @template T | |
| 214 | * @param Closure(string): T $parse | |
| 215 | * @return T | |
| 216 | * @throws ConfigurationException | |
| 217 | */ | |
| 218 | public function readParsedValue(string $key, Closure $parse): mixed | |
| 219 | { | |
| 220 | $raw = $this->readString($key); | |
| 221 | ||
| 222 | try { | |
| 223 | return $parse($raw); | |
| 224 | } catch (StewartException $e) { | |
| 225 | throw ConfigurationException::createForReason(ConfigurationError::KeyParseFailed, $e, path: $this->buildKeyPath($key)); | |
| 226 | } | |
| 227 | } | |
| 228 | ||
| 229 | private function createKeyInvalidFailure(string $key, string $expected): ConfigurationException | |
| 230 | { | |
| 231 | return ConfigurationException::createForReason(ConfigurationError::KeyInvalid, path: $this->buildKeyPath($key), expected: $expected); | |
| 232 | } | |
| 233 | ||
| 234 | // The schema already guarantees these types; a mismatch is a bug, not a user error. | |
| 235 | private function createSchemaMismatchFailure(string $key): LogicException | |
| 236 | { | |
| 237 | return new LogicException(\sprintf('The config schema and its reader disagree on %s.', $this->buildKeyPath($key))); | |
| 238 | } | |
| 239 | ||
| 240 | private function buildKeyPath(string $key): string | |
| 241 | { | |
| 242 | return $this->path === '' ? $key : $this->path . '.' . $key; | |
| 243 | } | |
| 244 | } |