Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.29% covered (success)
94.29%
33 / 35
81.82% covered (warning)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Values
94.29% covered (success)
94.29%
33 / 35
81.82% covered (warning)
81.82%
9 / 11
23.10
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
 quantity
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 money
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 code
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 codeListElement
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 dateTime
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 date
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 iri
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 individual
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 ref
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 term
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Model\Builder;
6
7use DateTimeInterface;
8use LambdaTwelve\OneRecord\Model\LocalRef;
9use LambdaTwelve\OneRecord\Model\ModelException;
10use LambdaTwelve\OneRecord\Rdf\Iri;
11use LambdaTwelve\OneRecord\Rdf\Literal;
12use LambdaTwelve\OneRecord\Spec\Namespaces;
13use LambdaTwelve\OneRecord\Vocabulary\Generated\Cargo;
14use LambdaTwelve\OneRecord\Vocabulary\Vocabulary;
15
16/**
17 * The value shapes every mapper needs, so hosts never assemble ONE Record
18 * JSON by hand: measured quantities, money, code-list members, codes from
19 * lists ONE Record does not publish, dates, references.
20 */
21final class Values
22{
23    private function __construct() {}
24
25    /**
26     * cargo:Value: a number with a unit from the MeasurementUnitCode list
27     * (`MeasurementUnitCode::KGM`), or any unit IRI.
28     */
29    public static function quantity(float|int $number, string $unitIri): Embedded
30    {
31        return Embedded::of(Cargo::Value)
32            ->set(Cargo::numericalValue, (float) $number)
33            ->set(Cargo::unit, new Iri($unitIri));
34    }
35
36    /**
37     * cargo:CurrencyValue. CurrencyCode is an open list with no published
38     * members, so the IRI follows the code-list pattern:
39     * .../code-lists/CurrencyCode#EUR (see spec question 11).
40     */
41    public static function money(float|int $amount, string $currencyCode): Embedded
42    {
43        if (preg_match('/^[A-Z]{3}$/', $currencyCode) !== 1) {
44            throw new ModelException(\sprintf('"%s" is not an ISO 4217 currency code.', $currencyCode));
45        }
46
47        return Embedded::of(Cargo::CurrencyValue)
48            ->set(Cargo::numericalValue, (float) $amount)
49            ->set(Cargo::currencyUnit, new Iri(Namespaces::codeList('CurrencyCode', $currencyCode)));
50    }
51
52    /**
53     * A member of a ONE Record code list by list name and code, checked
54     * against the published members unless the list is open.
55     */
56    public static function code(string $list, string $code, ?Vocabulary $vocabulary = null): Iri
57    {
58        $vocabulary ??= Vocabulary::default();
59        $info = $vocabulary->codeList(Namespaces::CODE_LISTS . $list);
60        if ($info === null) {
61            throw new ModelException(\sprintf('"%s" is not a ONE Record code list.', $list));
62        }
63        if (!$info->open && !$info->hasCode($code)) {
64            throw new ModelException(\sprintf('"%s" is not a published code of the closed list %s.', $code, $list));
65        }
66
67        return new Iri($info->iriOf($code));
68    }
69
70    /**
71     * cargo:CodeListElement, for codes from lists ONE Record does not publish
72     * (ISO countries, IATA airports, HS codes).
73     */
74    public static function codeListElement(string $code, string $listName, ?string $listVersion = null, ?string $description = null): Embedded
75    {
76        return Embedded::of(Cargo::CodeListElement)
77            ->set(Cargo::code, $code)
78            ->set(Cargo::codeListName, $listName)
79            ->setIf(Cargo::codeListVersion, $listVersion)
80            ->setIf(Cargo::codeDescription, $description);
81    }
82
83    public static function dateTime(DateTimeInterface $value): Literal
84    {
85        return Literal::dateTime($value);
86    }
87
88    /**
89     * xsd:date, for properties whose range is a date rather than a date-time.
90     */
91    public static function date(DateTimeInterface $value): Literal
92    {
93        return new Literal($value->format('Y-m-d'), Namespaces::xsd('date'));
94    }
95
96    public static function iri(string $iri): Iri
97    {
98        return new Iri($iri);
99    }
100
101    /**
102     * A named individual of the ontology (cargo:ACTUAL, cargo:MASTER, ...).
103     */
104    public static function individual(string $iri, ?Vocabulary $vocabulary = null): Iri
105    {
106        if (($vocabulary ?? Vocabulary::default())->individual($iri) === null) {
107            throw new ModelException(\sprintf('"%s" is not a named individual of the ontology.', $iri));
108        }
109
110        return new Iri($iri);
111    }
112
113    public static function ref(string $localKey): LocalRef
114    {
115        return LocalRef::to($localKey);
116    }
117
118    /**
119     * Normalise what a mapper passes into a term: PHP scalars become typed
120     * literals, dates become xsd:dateTime, Iri/Literal/LocalRef/Embedded pass through.
121     */
122    public static function term(mixed $value): Literal|Iri|LocalRef|Embedded
123    {
124        return match (true) {
125            $value instanceof Literal, $value instanceof Iri, $value instanceof LocalRef, $value instanceof Embedded => $value,
126            \is_string($value) => Literal::string($value),
127            \is_bool($value) => Literal::boolean($value),
128            \is_int($value) => Literal::integer($value),
129            \is_float($value) => Literal::double($value),
130            $value instanceof DateTimeInterface => Literal::dateTime($value),
131            default => throw new ModelException(\sprintf('Cannot use a value of type %s in a logistics object; pass a scalar, DateTimeInterface, Iri, Literal, LocalRef or Embedded.', get_debug_type($value))),
132        };
133    }
134}