packages/requirements/src/Config/SchemaValidator.php

1<?php
2
3declare(strict_types=1);
4
5namespace Requirements\Config;
6
7use JsonException;
8use Requirements\Input\InvalidInputException;
9use stdClass;
10
11/**
12 * Validates documents against the bundled JSON Schemas and any local schema they declare.
13 *
14 * A declared $schema is either the bundled schema's published URI, which resolves offline, or
15 * a local JSON Schema path that adds constraints to the bundled schema.
16 */
17final class SchemaValidator
18{
19    /**
20     * The published location of the bundled schemas, accepted as $schema without a download.
21     */
22    public const BASE = 'https://raw.githubusercontent.com/k-kinzal/ztd-query-php/main/packages/requirements/schemas/';
23
24    /**
25     * Validates a document.
26     *
27     * @param mixed $data The decoded document
28     * @param string $kind "config" or "definition", naming the bundled schema
29     * @param string $file The document file
30     *
31     * @throws InvalidInputException When the document breaks a schema, or declares a remote or unreadable one
32     * @throws JsonException When a schema is not JSON
33     */
34    public function validate(mixed $data, string $kind, string $file): void
35    {
36        $schema = new JsonSchemaFile();
37        $schema->validate($data, self::path($kind . '.schema.json'), $file);
38        if ($data instanceof stdClass && isset($data->{'$schema'})) {
39            $declared = $data->{'$schema'};
40            if (!is_string($declared)) {
41                throw new InvalidInputException("$file: \$schema must be a string.");
42            }
43            if ($declared === self::BASE . $kind . '.schema.json' || ($kind === 'definition' && DocumentReader::isMarkdown($file) && $declared === self::BASE . 'definition.document.yaml')) {
44                return;
45            }
46            if (str_contains($declared, '://')) {
47                throw new InvalidInputException("$file: unknown \$schema '$declared'; use the bundled schema URI or a local JSON Schema path.");
48            }
49            $schema->validate($data, str_starts_with($declared, '/') ? $declared : dirname($file) . '/' . $declared, $file);
50        }
51    }
52
53    /**
54     * Returns the path of a bundled schema.
55     *
56     * @param string $name The schema file name
57     *
58     * @return string The path within the package's schemas directory
59     */
60    public static function path(string $name): string
61    {
62        return dirname(__DIR__, 2) . '/schemas/' . $name;
63    }
64}
65