Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
RootSpecHandler
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
9 / 9
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getInfoSpec
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getLicenseSpec
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getContactSpec
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getServerSpec
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getPathsSpec
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getRouteSpec
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getComponentsSpec
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace MediaWiki\Rest\Handler;
4
5use MediaWiki\Config\Config;
6use MediaWiki\Config\ServiceOptions;
7use MediaWiki\MainConfigNames;
8use MediaWiki\Rest\ResponseFactory;
9use MediaWiki\Rest\SimpleHandler;
10use MediaWiki\Rest\Validator\Validator;
11
12/**
13 * Core REST API endpoint that outputs an OpenAPI spec of a set of routes.
14 */
15class RootSpecHandler extends SimpleHandler {
16    /**
17     * @internal
18     */
19    private const CONSTRUCTOR_OPTIONS = [
20        MainConfigNames::RightsUrl,
21        MainConfigNames::RightsText,
22        MainConfigNames::EmergencyContact,
23        MainConfigNames::Sitename,
24    ];
25
26    /** @var ServiceOptions */
27    private ServiceOptions $options;
28
29    /**
30     * @param Config $config
31     */
32    public function __construct( Config $config ) {
33        $options = new ServiceOptions( self::CONSTRUCTOR_OPTIONS, $config );
34        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
35        $this->options = $options;
36    }
37
38    public function run(): array {
39        // TODO: implement caching, get cache key from Router.
40
41        return [
42            'openapi' => '3.0.0',
43            'info' => $this->getInfoSpec(),
44            'servers' => $this->getServerSpec(),
45            'paths' => $this->getPathsSpec(),
46            'components' => $this->getComponentsSpec(),
47        ];
48    }
49
50    private function getInfoSpec(): array {
51        return [
52            'title' => $this->options->get( MainConfigNames::Sitename ),
53            'version' => MW_VERSION,
54            'license' => $this->getLicenseSpec(),
55            'contact' => $this->getContactSpec(),
56        ];
57    }
58
59    private function getLicenseSpec(): array {
60        return [
61            'name' => $this->options->get( MainConfigNames::RightsText ),
62            'url' => $this->options->get( MainConfigNames::RightsUrl ),
63        ];
64    }
65
66    private function getContactSpec(): array {
67        return [
68            'email' => $this->options->get( MainConfigNames::EmergencyContact ),
69        ];
70    }
71
72    private function getServerSpec(): array {
73        return [
74            [
75                'url' => $this->getRouter()->getRouteUrl( '/' ),
76            ]
77        ];
78    }
79
80    private function getPathsSpec(): array {
81        $specs = [];
82
83        foreach ( $this->getRouter()->getAllRoutes() as $route ) {
84            $path = $route['path'];
85            $methods = $route['method'] ?? [ 'get' ];
86
87            foreach ( (array)$methods as $mth ) {
88                $mth = strtolower( $mth );
89                $specs[ $path ][ $mth ] = $this->getRouteSpec( $route, $mth );
90            }
91        }
92
93        return $specs;
94    }
95
96    private function getRouteSpec( array $handlerObjectSpec, string $method ): array {
97        $handler = $this->getRouter()->instantiateHandlerObject( $handlerObjectSpec );
98        $handler->initContext( $this->getRouter(), $handlerObjectSpec );
99
100        $operationSpec = $handler->getOpenApiSpec( $method );
101
102        $overrides = array_intersect_key(
103            $handlerObjectSpec,
104            array_flip( [ 'description', 'summary', 'tags', 'deprecated', 'externalDocs', 'security' ] )
105        );
106
107        $operationSpec = $overrides + $operationSpec;
108
109        return $operationSpec;
110    }
111
112    private function getComponentsSpec() {
113        $components = [];
114
115        // XXX: also collect reusable components from handler specs (but how to avoid name collisions?).
116        $componentsSources = [
117            [ 'schemas' => Validator::getParameterTypeSchemas() ],
118            ResponseFactory::getResponseComponents()
119        ];
120
121        // 2D merge
122        foreach ( $componentsSources as $cmps ) {
123            foreach ( $cmps as $name => $cmp ) {
124                $components[$name] = array_merge( $components[$name] ?? [], $cmp );
125            }
126        }
127
128        return $components;
129    }
130
131}