packages/requirements/src/Config/Loader.php

1<?php
2
3declare(strict_types=1);
4
5namespace Requirements\Config;
6
7use JsonException;
8use Requirements\Input\Fields;
9use Requirements\Input\InvalidInputException;
10use Requirements\Model\Project;
11use Requirements\Source\Registry as SourceRegistry;
12use Requirements\Test\Registry as RunnerRegistry;
13use Requirements\Test\RunnerConfig;
14use Symfony\Component\Yaml\Exception\ParseException;
15
16/**
17 * Loads a project from its requirements.yaml and the definition files it names.
18 *
19 * Every document is validated against its schema, every item against the definition rules,
20 * links between items and to runners are resolved, and each source format and runner
21 * extension must be registered.
22 */
23final class Loader
24{
25    /**
26     * Loads and validates a project.
27     *
28     * @param string $file The configuration file
29     *
30     * @return Project The project
31     *
32     * @throws InvalidInputException When the configuration, a definition or a link is invalid
33     * @throws JsonException When a document cannot be converted
34     * @throws ParseException When a YAML document is malformed
35     */
36    public function load(string $file): Project
37    {
38        $path = realpath($file);
39        if ($path === false) {
40            throw new InvalidInputException("Configuration does not exist: $file");
41        }
42        $directory = dirname($path);
43        $data = $this->document($path, 'config');
44        Fields::keys($data, ['$schema', 'version', 'definitions', 'bootstrap', 'extensions', 'runners', 'coverage', 'markdown'], 'configuration');
45        if (isset($data['bootstrap'])) {
46            (new Bootstrap())->load($directory . '/' . Fields::text($data, 'bootstrap'));
47        }
48        $markdown = Fields::mapping($data['markdown'] ?? [], 'markdown');
49        $definitions = (new DefinitionReader())->read($directory, Fields::strings($data['definitions'] ?? [], 'definitions'), $markdown);
50        $runners = [];
51        foreach (Fields::mapping($data['runners'] ?? [], 'runners') as $name => $runner) {
52            $runners[$name] = RunnerConfig::from($runner, $directory);
53        }
54        (new LinkValidator())->validate($definitions->items, $runners);
55        $extensions = Fields::mapping($data['extensions'] ?? [], 'extensions');
56        Fields::keys($extensions, ['sources', 'runners'], 'extensions');
57        $coverage = Fields::mapping($data['coverage'] ?? [], 'coverage');
58        Fields::keys($coverage, ['minimum', 'diff_minimum', 'sources'], 'coverage');
59        $thresholds = (new CoverageThresholds())->read($coverage['sources'] ?? [], $definitions->sources);
60        $sourceClasses = ExtensionClasses::read($extensions['sources'] ?? []);
61        $runnerClasses = ExtensionClasses::read($extensions['runners'] ?? []);
62        $sourceRegistry = new SourceRegistry($sourceClasses);
63        $runnerRegistry = new RunnerRegistry($runnerClasses);
64        foreach ($definitions->sources as $source) {
65            $sourceRegistry->get($source->format);
66        }
67        foreach ($runners as $runner) {
68            $runnerRegistry->get($runner->extension);
69        }
70        return new Project($directory, $definitions->items, $definitions->sources, $runners, $sourceClasses, $runnerClasses, Fields::percentage($coverage['minimum'] ?? 0, 'coverage.minimum'), Fields::percentage($coverage['diff_minimum'] ?? 0, 'coverage.diff_minimum'), $thresholds, [$path, ...$definitions->files], $markdown);
71    }
72
73    /**
74     * Reads and validates one document as a mapping.
75     *
76     * @param string $path The document file
77     * @param string $kind "config" or "definition", naming the schema
78     * @param array<string, mixed> $markdown The markdown options
79     *
80     * @return array<string, mixed> The document
81     *
82     * @throws InvalidInputException When the document breaks its schema or is not a mapping
83     * @throws JsonException When the document cannot be converted
84     * @throws ParseException When a YAML document is malformed
85     */
86    public function document(string $path, string $kind = 'definition', array $markdown = []): array
87    {
88        return DocumentReader::mapping((new DocumentReader())->read($path, $kind, $markdown), $path);
89    }
90}
91