Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
ApiError
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
3 / 3
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fromGeneratedException
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 decodeMessage
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3declare(strict_types=1);
4
5namespace ForgeDashboard\Exception;
6
7use ForgeDashboard\Generated\ApiException;
8use Throwable;
9
10/**
11 * Thrown for any forge-dashboard response carrying an error body --
12 * every 4xx/5xx in the spec shares the same {error} shape
13 * (rules/sdk-generation.md's "Client shape": map errors to typed
14 * exceptions carrying the HTTP status and the parsed error body, never a
15 * bare string or the raw response).
16 */
17final class ApiError extends \RuntimeException
18{
19    public function __construct(
20        public readonly int $statusCode,
21        string $apiMessage,
22        ?Throwable $previous = null,
23    ) {
24        $message = \sprintf('forge-dashboard: %d: %s', $statusCode, $apiMessage);
25
26        parent::__construct($message, $statusCode, $previous);
27    }
28
29    /**
30     * Builds an ApiError from the ApiException the generated Api classes
31     * throw on any non-2xx response. Prefers the deserialized Error model
32     * openapi-generator attaches for status codes the spec documents
33     * explicitly; falls back to decoding the raw JSON body itself for a
34     * status code the spec doesn't list (a 429 or 500, say), since the
35     * generated code never deserializes those.
36     */
37    public static function fromGeneratedException(ApiException $exception): self
38    {
39        return new self(
40            statusCode: $exception->getCode(),
41            apiMessage: self::decodeMessage($exception),
42            previous: $exception,
43        );
44    }
45
46    private static function decodeMessage(ApiException $exception): string
47    {
48        $responseObject = $exception->getResponseObject();
49        if (\is_object($responseObject) && method_exists($responseObject, 'getError')) {
50            $error = $responseObject->getError();
51            if (\is_string($error)) {
52                return $error;
53            }
54        }
55
56        $body = $exception->getResponseBody();
57        $raw = \is_string($body) ? $body : (string) json_encode($body);
58        $decoded = json_decode($raw, true);
59        if (\is_array($decoded) && isset($decoded['error']) && \is_string($decoded['error'])) {
60            return $decoded['error'];
61        }
62
63        return $exception->getMessage();
64    }
65}