Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.36% covered (success)
96.36%
53 / 55
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Notification
96.36% covered (success)
96.36%
53 / 55
75.00% covered (warning)
75.00%
3 / 4
24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromJsonLd
93.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
10.02
 toJsonLd
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
 collect
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Api;
6
7use LambdaTwelve\OneRecord\JsonLd\ExpandedDocument;
8use LambdaTwelve\OneRecord\JsonLd\JsonLd;
9use LambdaTwelve\OneRecord\JsonLd\JsonLdException;
10use LambdaTwelve\OneRecord\JsonLd\Nodes;
11use LambdaTwelve\OneRecord\JsonLd\Writer;
12use LambdaTwelve\OneRecord\Model\LogisticsObject;
13use LambdaTwelve\OneRecord\Rdf\Graph;
14use LambdaTwelve\OneRecord\Rdf\Iri;
15use LambdaTwelve\OneRecord\Rdf\Triple;
16use LambdaTwelve\OneRecord\Vocabulary\Generated\Api;
17
18/**
19 * api:Notification: what one node tells another about a created or updated
20 * object, a received event, or the status of an action request.
21 */
22final readonly class Notification
23{
24    /**
25     * @param list<string> $changedProperties property IRIs
26     * @param list<Iri> $logisticsEvents
27     * @param ?LogisticsObject $body the full object, when the subscription asked for it
28     */
29    public function __construct(
30        public NotificationEventType $eventType,
31        public ?Iri $logisticsObject = null,
32        public ?string $logisticsObjectType = null,
33        public ?Iri $triggeredBy = null,
34        public array $changedProperties = [],
35        public array $logisticsEvents = [],
36        public ?LogisticsObject $body = null,
37    ) {}
38
39    /**
40     * @param string|array<string, mixed>|ExpandedDocument $document
41     */
42    public static function fromJsonLd(string|array|ExpandedDocument $document): self
43    {
44        try {
45            $expanded = $document instanceof ExpandedDocument ? $document : JsonLd::expand($document);
46        } catch (JsonLdException $e) {
47            throw InvalidDocument::because('Invalid body request', $e->getMessage());
48        }
49        if (!\in_array(Api::Notification, $expanded->rootTypes(), true)) {
50            throw InvalidDocument::because('Invalid resource', 'The body is not an api:Notification.');
51        }
52        $graph = $expanded->graph;
53        $root = $expanded->root;
54        $typeIri = Nodes::iri($graph, $root, Api::hasEventType);
55        $eventType = $typeIri !== null ? NotificationEventType::tryFromString($typeIri->value) : null;
56        if ($eventType === null) {
57            throw InvalidDocument::because('Invalid resource', 'api:hasEventType must be a notification event type.', Api::hasEventType);
58        }
59        $objectIri = Nodes::iri($graph, $root, Api::hasLogisticsObject);
60        $body = null;
61        if ($objectIri !== null) {
62            // Anything said about the object beyond its URI is the embedded body (sendLogisticsObjectBody).
63            $about = $graph->about($objectIri);
64            if ($about !== []) {
65                $sub = new Graph();
66                $seen = [];
67                self::collect($graph, $objectIri, $sub, $seen);
68                $body = new LogisticsObject($objectIri, $sub);
69            }
70        }
71        $objectType = Nodes::string($graph, $root, Api::hasLogisticsObjectType);
72        if ($objectType === null && $body !== null) {
73            $objectType = $body->mostSpecificType();
74        }
75
76        return new self(
77            $eventType,
78            $objectIri,
79            $objectType,
80            Nodes::iri($graph, $root, Api::isTriggeredBy),
81            Nodes::strings($graph, $root, Api::hasChangedProperty),
82            Nodes::iris($graph, $root, Api::hasLogisticsEvent),
83            $body,
84        );
85    }
86
87    /**
88     * @return array<string, mixed>
89     */
90    public function toJsonLd(): array
91    {
92        $node = ['@context' => Nodes::context(), '@type' => 'api:Notification', 'api:hasEventType' => Nodes::ref(Nodes::compact($this->eventType->value))];
93        if ($this->logisticsObject !== null) {
94            if ($this->body !== null) {
95                $embedded = (new Writer())->write($this->body->graph, $this->body->iri, \LambdaTwelve\OneRecord\JsonLd\Context::oneRecord(), includeContext: false);
96                $node['api:hasLogisticsObject'] = $embedded;
97            } else {
98                $node['api:hasLogisticsObject'] = Nodes::ref($this->logisticsObject);
99            }
100        }
101        if ($this->logisticsObjectType !== null) {
102            $node['api:hasLogisticsObjectType'] = Nodes::anyUri($this->logisticsObjectType);
103        }
104        if ($this->triggeredBy !== null) {
105            $node['api:isTriggeredBy'] = Nodes::ref($this->triggeredBy);
106        }
107        if ($this->changedProperties !== []) {
108            $node['api:hasChangedProperty'] = array_map(Nodes::anyUri(...), $this->changedProperties);
109        }
110        if ($this->logisticsEvents !== []) {
111            $refs = array_map(static fn(Iri $i): array => Nodes::ref($i), $this->logisticsEvents);
112            $node['api:hasLogisticsEvent'] = \count($refs) === 1 ? $refs[0] : $refs;
113        }
114
115        return $node;
116    }
117
118    /**
119     * One visited set for the whole walk: a set per branch would terminate cycles but
120     * revisit every shared descendant, which is exponential on a diamond-shaped graph (AR-004).
121     *
122     * @param array<string, true> $seen
123     */
124    private static function collect(Graph $graph, Iri|\LambdaTwelve\OneRecord\Rdf\BlankNode $node, Graph $into, array &$seen): void
125    {
126        $seen[$node->toNTriples()] = true;
127        foreach ($graph->about($node) as $triple) {
128            $into->add(new Triple($triple->subject, $triple->predicate, $triple->object));
129            $object = $triple->object;
130            if (($object instanceof Iri || $object instanceof \LambdaTwelve\OneRecord\Rdf\BlankNode) && !isset($seen[$object->toNTriples()])) {
131                self::collect($graph, $object, $into, $seen);
132            }
133        }
134    }
135}