Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.18% covered (success)
93.18%
41 / 44
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
LocalGraph
93.18% covered (success)
93.18%
41 / 44
70.00% covered (warning)
70.00%
7 / 10
25.20
0.00% covered (danger)
0.00%
0 / 1
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 root
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 rootKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 keys
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 graphOf
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validate
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
8
 resolve
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 rewrite
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Model;
6
7use LambdaTwelve\OneRecord\Model\Builder\ObjectBuilder;
8use LambdaTwelve\OneRecord\Rdf\Graph;
9use LambdaTwelve\OneRecord\Rdf\Iri;
10use LambdaTwelve\OneRecord\Rdf\Triple;
11
12/**
13 * A set of logistics objects that refer to each other by local key, before
14 * any of them has a public URI.
15 *
16 * A host maps its business data into one of these (a waybill, its shipment,
17 * the pieces, the parties), then resolves it with an IriMinter when
18 * publishing. Until then every object's subject is `local:<key>` and
19 * cross-references are `local:<other key>`, so the graph is plain data that
20 * needs no server to build or test.
21 */
22final class LocalGraph
23{
24    /** @var array<string, Graph> local key => the object's triples, subject local:<key> */
25    private array $objects = [];
26
27    private ?string $root = null;
28
29    public static function create(): self
30    {
31        return new self();
32    }
33
34    /**
35     * @return $this
36     */
37    public function add(string $key, ObjectBuilder|Graph $object): self
38    {
39        $ref = new LocalRef($key);
40        if (isset($this->objects[$key])) {
41            throw new ModelException(\sprintf('The graph already has an object with key "%s".', $key));
42        }
43        $this->objects[$key] = $object instanceof ObjectBuilder ? $object->buildGraph($ref->iri()) : $object;
44        $this->root ??= $key;
45
46        return $this;
47    }
48
49    /**
50     * The object partners are told about (the master waybill, say). Defaults to the first added.
51     *
52     * @return $this
53     */
54    public function root(string $key): self
55    {
56        if (!isset($this->objects[$key])) {
57            throw new ModelException(\sprintf('The graph has no object with key "%s" to use as root.', $key));
58        }
59        $this->root = $key;
60
61        return $this;
62    }
63
64    public function rootKey(): string
65    {
66        return $this->root ?? throw new ModelException('The graph is empty.');
67    }
68
69    /**
70     * @return list<string>
71     */
72    public function keys(): array
73    {
74        return array_keys($this->objects);
75    }
76
77    public function has(string $key): bool
78    {
79        return isset($this->objects[$key]);
80    }
81
82    public function graphOf(string $key): Graph
83    {
84        return $this->objects[$key] ?? throw new ModelException(\sprintf('The graph has no object with key "%s".', $key));
85    }
86
87    /**
88     * Every local reference must point at an object in the graph; dangling
89     * references would publish a link to nothing.
90     */
91    public function validate(): void
92    {
93        if ($this->objects === []) {
94            throw new ModelException('The graph is empty.');
95        }
96        foreach ($this->objects as $key => $graph) {
97            foreach ($graph as $triple) {
98                foreach ([$triple->subject, $triple->object] as $term) {
99                    if ($term instanceof Iri && LocalRef::isLocal($term)) {
100                        $target = LocalRef::keyOf($term);
101                        if (!isset($this->objects[$target])) {
102                            throw new ModelException(\sprintf('Object "%s" refers to "%s", which the graph does not contain.', $key, $target));
103                        }
104                    }
105                }
106            }
107        }
108    }
109
110    /**
111     * Mint a URI for every object (existing URIs win) and rewrite every local
112     * reference to the URI its key resolved to.
113     *
114     * @param array<string, Iri> $existing local key => URI already published under
115     */
116    public function resolve(IriMinter $minter, array $existing = []): ResolvedGraph
117    {
118        $this->validate();
119        $iris = [];
120        foreach ($this->objects as $key => $graph) {
121            $types = array_map(static fn(Iri $t): string => $t->value, $graph->typesOf((new LocalRef($key))->iri()));
122            sort($types, SORT_STRING);
123            $iris[$key] = $existing[$key] ?? $minter->mint($key, $types);
124        }
125
126        $objects = [];
127        foreach ($this->objects as $key => $graph) {
128            $rewritten = new Graph();
129            foreach ($graph as $triple) {
130                $rewritten->add(new Triple(
131                    self::rewrite($triple->subject, $iris),
132                    $triple->predicate,
133                    self::rewrite($triple->object, $iris),
134                ));
135            }
136            $objects[$key] = new LogisticsObject($iris[$key], $rewritten);
137        }
138
139        return new ResolvedGraph($this->rootKey(), $objects);
140    }
141
142    /**
143     * @template T of \LambdaTwelve\OneRecord\Rdf\Term
144     * @param T $term
145     * @param array<string, Iri> $iris
146     * @return T|Iri
147     */
148    private static function rewrite(\LambdaTwelve\OneRecord\Rdf\Term $term, array $iris): \LambdaTwelve\OneRecord\Rdf\Term
149    {
150        if ($term instanceof Iri && LocalRef::isLocal($term)) {
151            return $iris[LocalRef::keyOf($term)];
152        }
153
154        return $term;
155    }
156}