Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.75% covered (success)
93.75%
45 / 48
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ErrorDocument
93.75% covered (success)
93.75%
45 / 48
66.67% covered (warning)
66.67%
6 / 9
21.11
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 write
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 node
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 nodes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 readFrom
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 readNode
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 read
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 language
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 xsd
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Api;
6
7use LambdaTwelve\OneRecord\JsonLd\JsonLd;
8use LambdaTwelve\OneRecord\JsonLd\JsonLdException;
9use LambdaTwelve\OneRecord\JsonLd\Nodes;
10use LambdaTwelve\OneRecord\Rdf\BlankNode;
11use LambdaTwelve\OneRecord\Rdf\Graph;
12use LambdaTwelve\OneRecord\Rdf\Iri;
13use LambdaTwelve\OneRecord\Spec\ApiFeatures;
14use LambdaTwelve\OneRecord\Spec\ApiVersion;
15use LambdaTwelve\OneRecord\Spec\Namespaces;
16use 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 */
23final 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}