Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
93.75% |
45 / 48 |
|
66.67% |
6 / 9 |
CRAP | |
0.00% |
0 / 1 |
| ErrorDocument | |
93.75% |
45 / 48 |
|
66.67% |
6 / 9 |
21.11 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| write | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| node | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
8 | |||
| nodes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| readFrom | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| readNode | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
| read | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| language | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| xsd | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace LambdaTwelve\OneRecord\Api; |
| 6 | |
| 7 | use LambdaTwelve\OneRecord\JsonLd\JsonLd; |
| 8 | use LambdaTwelve\OneRecord\JsonLd\JsonLdException; |
| 9 | use LambdaTwelve\OneRecord\JsonLd\Nodes; |
| 10 | use LambdaTwelve\OneRecord\Rdf\BlankNode; |
| 11 | use LambdaTwelve\OneRecord\Rdf\Graph; |
| 12 | use LambdaTwelve\OneRecord\Rdf\Iri; |
| 13 | use LambdaTwelve\OneRecord\Spec\ApiFeatures; |
| 14 | use LambdaTwelve\OneRecord\Spec\ApiVersion; |
| 15 | use LambdaTwelve\OneRecord\Spec\Namespaces; |
| 16 | use LambdaTwelve\OneRecord\Vocabulary\Generated\Api; |
| 17 | |
| 18 | /** |
| 19 | * api:Error as JSON-LD: the body of every 4xx/5xx response, and the errors |
| 20 | * embedded in action requests and verifications. Severity is a 2.3 addition |
| 21 | * and is left out when 2.2 was negotiated. |
| 22 | */ |
| 23 | final class ErrorDocument |
| 24 | { |
| 25 | private function __construct() {} |
| 26 | |
| 27 | /** |
| 28 | * @return array<string, mixed> |
| 29 | */ |
| 30 | public static function write(Error $error, ApiVersion $version, string $language = 'en-US'): array |
| 31 | { |
| 32 | return [ |
| 33 | '@context' => [...Nodes::context(), 'api:hasResource' => ['@type' => 'xsd:anyURI'], 'api:hasProperty' => ['@type' => 'xsd:anyURI'], '@language' => $language], |
| 34 | ...self::node($error, $version), |
| 35 | ]; |
| 36 | } |
| 37 | |
| 38 | /** |
| 39 | * The error without its own @context, for embedding in another document. |
| 40 | * |
| 41 | * @return array<string, mixed> |
| 42 | */ |
| 43 | public static function node(Error $error, ApiVersion $version): array |
| 44 | { |
| 45 | $node = ['@type' => 'api:Error', 'api:hasTitle' => $error->title]; |
| 46 | if (ApiFeatures::available($version, ApiFeatures::ERROR_SEVERITY)) { |
| 47 | $node['api:hasSeverity'] = Nodes::ref(Nodes::compact($error->severity->value)); |
| 48 | } |
| 49 | $details = []; |
| 50 | foreach ($error->details as $detail) { |
| 51 | $item = ['@type' => 'api:ErrorDetail']; |
| 52 | if ($detail->code !== null) { |
| 53 | $item['api:hasCode'] = $detail->code; |
| 54 | } |
| 55 | if ($detail->message !== null) { |
| 56 | $item['api:hasMessage'] = $detail->message; |
| 57 | } |
| 58 | if ($detail->property !== null) { |
| 59 | $item['api:hasProperty'] = $detail->property; |
| 60 | } |
| 61 | if ($detail->resource !== null) { |
| 62 | $item['api:hasResource'] = $detail->resource; |
| 63 | } |
| 64 | $details[] = $item; |
| 65 | } |
| 66 | if ($details !== []) { |
| 67 | $node['api:hasErrorDetail'] = $details; |
| 68 | } |
| 69 | |
| 70 | return $node; |
| 71 | } |
| 72 | |
| 73 | /** |
| 74 | * @param list<Error> $errors |
| 75 | * @return list<array<string, mixed>> |
| 76 | */ |
| 77 | public static function nodes(array $errors, ApiVersion $version): array |
| 78 | { |
| 79 | return array_map(static fn(Error $e): array => self::node($e, $version), $errors); |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Reads the api:Error nodes referenced from $node by $predicate. |
| 84 | * |
| 85 | * @return list<Error> |
| 86 | */ |
| 87 | public static function readFrom(Graph $graph, Iri|BlankNode $node, string $predicate): array |
| 88 | { |
| 89 | $errors = []; |
| 90 | foreach (Nodes::nodes($graph, $node, $predicate) as $errorNode) { |
| 91 | $errors[] = self::readNode($graph, $errorNode); |
| 92 | } |
| 93 | |
| 94 | return $errors; |
| 95 | } |
| 96 | |
| 97 | public static function readNode(Graph $graph, Iri|BlankNode $node): Error |
| 98 | { |
| 99 | $title = Nodes::string($graph, $node, Api::hasTitle) ?? 'Error'; |
| 100 | $severityIri = Nodes::iri($graph, $node, Api::hasSeverity); |
| 101 | $severity = $severityIri !== null ? (Severity::tryFrom($severityIri->value) ?? Severity::Error) : Severity::Error; |
| 102 | $details = []; |
| 103 | foreach (Nodes::nodes($graph, $node, Api::hasErrorDetail) as $detailNode) { |
| 104 | $details[] = new ErrorDetail( |
| 105 | Nodes::string($graph, $detailNode, Api::hasCode), |
| 106 | Nodes::string($graph, $detailNode, Api::hasMessage), |
| 107 | Nodes::string($graph, $detailNode, Api::hasProperty), |
| 108 | Nodes::string($graph, $detailNode, Api::hasResource), |
| 109 | ); |
| 110 | } |
| 111 | |
| 112 | return new Error($title, $details, $severity); |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Reads a standalone api:Error body (an HTTP error response from a partner). |
| 117 | * |
| 118 | * @param string|array<string, mixed> $json |
| 119 | */ |
| 120 | public static function read(string|array $json): ?Error |
| 121 | { |
| 122 | try { |
| 123 | $document = JsonLd::expand($json); |
| 124 | } catch (JsonLdException) { |
| 125 | return null; |
| 126 | } |
| 127 | if (!\in_array(Api::Error, $document->rootTypes(), true)) { |
| 128 | return null; |
| 129 | } |
| 130 | |
| 131 | return self::readNode($document->graph, $document->root); |
| 132 | } |
| 133 | |
| 134 | public static function language(): string |
| 135 | { |
| 136 | return 'en-US'; |
| 137 | } |
| 138 | |
| 139 | /** @internal keeps Namespaces imported for the context of embedded errors */ |
| 140 | public static function xsd(): string |
| 141 | { |
| 142 | return Namespaces::XSD; |
| 143 | } |
| 144 | } |