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