Lines
100.00%
25 / 25
Functions and Methods
100.00%
7 / 7
Classes and Traits
100.00%
1 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| HomeAssistantUrl | 100.00% | 25 / 25 | 100.00% | 7 / 7 | 16 | 100.00% | 1 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| parse | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 1 | |||||
| tryParse | 100.00% | 12 / 12 | 100.00% | 1 / 1 | 5 | |||||
| reveal | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| __toString | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| prefixScheme | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 2 | |||||
| appendWebsocketPath | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 5 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Stewart\Client\Connection; | |
| 6 | ||
| 7 | use SensitiveParameter; | |
| 8 | use Stewart\Client\Exception\HaClientError; | |
| 9 | use Stewart\Client\Exception\HaClientException; | |
| 10 | use Stewart\Support\Url\UrlRedactor; | |
| 11 | use Stringable; | |
| 12 | ||
| 13 | final readonly class HomeAssistantUrl implements Stringable | |
| 14 | { | |
| 15 | private const string SCHEME = '~\A([a-z][a-z0-9+.-]*)://~i'; | |
| 16 | ||
| 17 | private const array WEBSOCKET_SCHEMES = ['http' => 'ws', 'https' => 'wss', 'ws' => 'ws', 'wss' => 'wss']; | |
| 18 | ||
| 19 | private const string DEFAULT_SCHEME = 'ws'; | |
| 20 | ||
| 21 | private const string WEBSOCKET_PATH = '/api/websocket'; | |
| 22 | ||
| 23 | private const string WEBSOCKET_PATH_ENDING = '/websocket'; | |
| 24 | ||
| 25 | private function __construct( | |
| 26 | #[SensitiveParameter] | |
| 27 | private string $url, | |
| 28 | private string $redacted, | |
| 29 | ) {} | |
| 30 | ||
| 31 | /** @throws HaClientException */ | |
| 32 | public static function parse(#[SensitiveParameter] string $address): self | |
| 33 | { | |
| 34 | return self::tryParse($address) ?? throw HaClientException::createForReason( | |
| 35 | HaClientError::UrlInvalid, | |
| 36 | url: UrlRedactor::redactCredentials(self::prefixScheme(trim($address))), | |
| 37 | ); | |
| 38 | } | |
| 39 | ||
| 40 | public static function tryParse(#[SensitiveParameter] string $address): ?self | |
| 41 | { | |
| 42 | $address = trim($address); | |
| 43 | ||
| 44 | if (preg_match(self::SCHEME, $address, $match) === 1) { | |
| 45 | $scheme = self::WEBSOCKET_SCHEMES[strtolower($match[1])] ?? null; | |
| 46 | ||
| 47 | if ($scheme === null) { | |
| 48 | return null; | |
| 49 | } | |
| 50 | ||
| 51 | $address = $scheme . '://' . substr($address, \strlen($match[0])); | |
| 52 | } | |
| 53 | ||
| 54 | $url = rtrim(self::prefixScheme($address), '/'); | |
| 55 | $parts = parse_url($url); | |
| 56 | ||
| 57 | if (!\is_array($parts) || ($parts['host'] ?? '') === '') { | |
| 58 | return null; | |
| 59 | } | |
| 60 | ||
| 61 | $url = self::appendWebsocketPath($url, $parts['path'] ?? '', $parts['query'] ?? null, $parts['fragment'] ?? null); | |
| 62 | ||
| 63 | return new self($url, UrlRedactor::redactCredentials($url)); | |
| 64 | } | |
| 65 | ||
| 66 | public function reveal(): string | |
| 67 | { | |
| 68 | return $this->url; | |
| 69 | } | |
| 70 | ||
| 71 | public function __toString(): string | |
| 72 | { | |
| 73 | return $this->redacted; | |
| 74 | } | |
| 75 | ||
| 76 | private static function prefixScheme(string $address): string | |
| 77 | { | |
| 78 | return preg_match(self::SCHEME, $address) === 1 ? $address : self::DEFAULT_SCHEME . '://' . $address; | |
| 79 | } | |
| 80 | ||
| 81 | // Appended to the path: appending to the whole URL would put it after the query string. | |
| 82 | private static function appendWebsocketPath(string $url, string $path, ?string $query, ?string $fragment): string | |
| 83 | { | |
| 84 | if (str_ends_with(rtrim($path, '/'), self::WEBSOCKET_PATH_ENDING)) { | |
| 85 | return $url; | |
| 86 | } | |
| 87 | ||
| 88 | $tail = ($query === null ? '' : '?' . $query) . ($fragment === null ? '' : '#' . $fragment); | |
| 89 | $head = $tail === '' ? $url : substr($url, 0, \strlen($url) - \strlen($tail)); | |
| 90 | ||
| 91 | return rtrim($head, '/') . self::WEBSOCKET_PATH . $tail; | |
| 92 | } | |
| 93 | } |