Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
74.36% |
29 / 39 |
|
0.00% |
0 / 2 |
CRAP | |
0.00% |
0 / 1 |
| CreateLogisticsObjectEndpoint | |
74.36% |
29 / 39 |
|
0.00% |
0 / 2 |
17.30 | |
0.00% |
0 / 1 |
| handle | |
76.67% |
23 / 30 |
|
0.00% |
0 / 1 |
11.27 | |||
| validate | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
4.59 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace LambdaTwelve\OneRecord\Server\Endpoint; |
| 6 | |
| 7 | use InvalidArgumentException; |
| 8 | use LambdaTwelve\OneRecord\JsonLd\JsonLdException; |
| 9 | use LambdaTwelve\OneRecord\Model\LogisticsObject; |
| 10 | use LambdaTwelve\OneRecord\Model\ModelException; |
| 11 | use LambdaTwelve\OneRecord\Server\Event\LogisticsObjectCreated; |
| 12 | use LambdaTwelve\OneRecord\Server\Http\HttpException; |
| 13 | use LambdaTwelve\OneRecord\Server\Http\Negotiated; |
| 14 | use LambdaTwelve\OneRecord\Server\Notification\Fanout; |
| 15 | use LambdaTwelve\OneRecord\Server\Spi\Action; |
| 16 | use LambdaTwelve\OneRecord\Server\Spi\Agent; |
| 17 | use LambdaTwelve\OneRecord\Server\Spi\StoreException; |
| 18 | use Psr\Http\Message\ResponseInterface; |
| 19 | use Psr\Http\Message\ServerRequestInterface; |
| 20 | |
| 21 | /** |
| 22 | * POST /logistics-objects. The spec marks it "internal only": it exists for |
| 23 | * hosts that create objects over HTTP from their own systems, so the access |
| 24 | * policy must say yes (it says no by default). A body with an @id under this |
| 25 | * server is honoured (predefined URIs, as NE:ONE allows); any other @id is |
| 26 | * refused; without one the server mints a URI. |
| 27 | */ |
| 28 | final class CreateLogisticsObjectEndpoint extends AbstractEndpoint |
| 29 | { |
| 30 | public function handle(ServerRequestInterface $request, Agent $agent, Negotiated $negotiated, array $parameters): ResponseInterface |
| 31 | { |
| 32 | $this->decide($agent, Action::CreateLogisticsObject, null); |
| 33 | (new \LambdaTwelve\OneRecord\Server\Http\ContentNegotiation($this->services->config))->bodyVersion($request, $negotiated); |
| 34 | $json = $this->services->body->json($request); |
| 35 | if (\array_key_exists('@graph', $json)) { |
| 36 | throw HttpException::badRequest('The body must not contain @graph; a single logistics object is expected.', '@graph'); |
| 37 | } |
| 38 | |
| 39 | $iri = null; |
| 40 | if (\array_key_exists('@id', $json)) { |
| 41 | // A present @id is honoured or refused, never silently replaced: a number, a list or an empty or |
| 42 | // malformed string is the client's mistake, not an omission (R14-004). |
| 43 | $declared = $json['@id']; |
| 44 | $message = 'An @id must be a logistics object URI under this server, or be left out so the server assigns one.'; |
| 45 | if (!\is_string($declared) || trim($declared) === '') { |
| 46 | throw HttpException::badRequest($message, '@id'); |
| 47 | } |
| 48 | try { |
| 49 | $relative = $this->services->config->relativePath(new \LambdaTwelve\OneRecord\Rdf\Iri(trim($declared))); |
| 50 | } catch (InvalidArgumentException) { |
| 51 | throw HttpException::badRequest($message, '@id'); |
| 52 | } |
| 53 | if ($relative === null || preg_match('#^logistics-objects/[^/?]+$#', $relative) !== 1) { |
| 54 | throw HttpException::badRequest($message, '@id'); |
| 55 | } |
| 56 | $iri = $this->services->config->logisticsObjectIri(substr($relative, \strlen('logistics-objects/'))); |
| 57 | unset($json['@id']); |
| 58 | } |
| 59 | $iri ??= $this->services->config->logisticsObjectIri($this->services->ids->next()); |
| 60 | |
| 61 | try { |
| 62 | $object = LogisticsObject::fromJsonLd($json, $iri)->withEmbeddedIds($this->services->embeddedIds); |
| 63 | } catch (JsonLdException|ModelException $e) { |
| 64 | throw HttpException::badRequest($e->getMessage()); |
| 65 | } |
| 66 | $this->validate($object); |
| 67 | |
| 68 | try { |
| 69 | $stored = $this->services->objects->create($object, $this->services->clock->now()); |
| 70 | } catch (StoreException $e) { |
| 71 | throw HttpException::conflict('A logistics object with this URI already exists.', $iri->value); |
| 72 | } |
| 73 | \LambdaTwelve\OneRecord\Server\Deprecations::log($stored->object->graph, $stored->object->iri, $this->services->vocabulary, $this->services->logger); |
| 74 | $this->services->dispatcher->dispatch(new LogisticsObjectCreated($stored, $agent->iri)); |
| 75 | // No action request causes a creation; fan-out then names the subscription request (AR-021). |
| 76 | (new Fanout($this->services))->logisticsObjectCreated($stored); |
| 77 | |
| 78 | return $this->services->responder->empty(201, $negotiated, ['Location' => $iri->value, 'Type' => $stored->object->mostSpecificType($this->services->vocabulary) ?? 'https://onerecord.iata.org/ns/cargo#LogisticsObject']); |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Model validation per the spec's implementation guidelines: the object |
| 83 | * must be typed with a logistics object class; properties the ontology does |
| 84 | * not know are refused (a 400), properties the class does not accept too. |
| 85 | */ |
| 86 | private function validate(LogisticsObject $object): void |
| 87 | { |
| 88 | $types = $object->types(); |
| 89 | if ($types === []) { |
| 90 | throw HttpException::badRequest('A logistics object must declare its @type.', '@type', 'Invalid resource'); |
| 91 | } |
| 92 | $vocabulary = $this->services->vocabulary; |
| 93 | if (array_filter($types, static fn(string $t): bool => $vocabulary->isLogisticsObjectClass($t)) === []) { |
| 94 | throw HttpException::badRequest(\sprintf('%s is not a logistics object class.', implode(', ', $types)), '@type', 'Invalid resource'); |
| 95 | } |
| 96 | // Root and every embedded node, properties, kinds, ranges and grammars: the same validator that |
| 97 | // judges a change or a posted event (R10-004). |
| 98 | // A nested logistics object (the spec's example A2 posts a Company with a Person inside) is kept as |
| 99 | // an embedded node rather than refused or split into objects of its own: spec question 33. |
| 100 | $violations = (new \LambdaTwelve\OneRecord\Model\GraphValidator($vocabulary))->validate($object->graph, $object->iri, nestedLogisticsObjects: true); |
| 101 | if ($violations !== []) { |
| 102 | throw HttpException::badRequest($violations[0]->message, $violations[0]->property, 'Invalid resource'); |
| 103 | } |
| 104 | } |
| 105 | } |