Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.81% |
73 / 77 |
|
90.00% |
9 / 10 |
CRAP | |
0.00% |
0 / 1 |
ModuleSpecHandler | |
94.81% |
73 / 77 |
|
90.00% |
9 / 10 |
19.05 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
run | |
76.47% |
13 / 17 |
|
0.00% |
0 / 1 |
4.21 | |||
getInfoSpec | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
getLicenseSpec | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getContactSpec | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getServerSpec | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
getPathsSpec | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getRouteSpec | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getComponentsSpec | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
getParamSettings | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest\Handler; |
4 | |
5 | use MediaWiki\Config\Config; |
6 | use MediaWiki\Config\ServiceOptions; |
7 | use MediaWiki\MainConfigNames; |
8 | use MediaWiki\Rest\LocalizedHttpException; |
9 | use MediaWiki\Rest\Module\Module; |
10 | use MediaWiki\Rest\RequestData; |
11 | use MediaWiki\Rest\ResponseFactory; |
12 | use MediaWiki\Rest\SimpleHandler; |
13 | use MediaWiki\Rest\Validator\Validator; |
14 | use Wikimedia\Message\MessageValue; |
15 | use Wikimedia\ParamValidator\ParamValidator; |
16 | |
17 | /** |
18 | * Core REST API endpoint that outputs an OpenAPI spec of a set of routes. |
19 | */ |
20 | class ModuleSpecHandler extends SimpleHandler { |
21 | |
22 | /** |
23 | * @internal |
24 | */ |
25 | private const CONSTRUCTOR_OPTIONS = [ |
26 | MainConfigNames::RightsUrl, |
27 | MainConfigNames::RightsText, |
28 | MainConfigNames::EmergencyContact, |
29 | MainConfigNames::Sitename, |
30 | ]; |
31 | |
32 | private ServiceOptions $options; |
33 | |
34 | public function __construct( Config $config ) { |
35 | $options = new ServiceOptions( self::CONSTRUCTOR_OPTIONS, $config ); |
36 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
37 | $this->options = $options; |
38 | } |
39 | |
40 | public function run( $moduleName, $version = '' ): array { |
41 | // TODO: implement caching, get cache key from Router. |
42 | |
43 | if ( $version !== '' ) { |
44 | $moduleName .= '/' . $version; |
45 | } |
46 | |
47 | if ( $moduleName === '-' ) { |
48 | // Hack that allows us to fetch a spec for the empty module prefix |
49 | $moduleName = ''; |
50 | } |
51 | |
52 | $module = $this->getRouter()->getModule( $moduleName ); |
53 | |
54 | if ( !$module ) { |
55 | throw new LocalizedHttpException( |
56 | MessageValue::new( 'rest-unknown-module' )->params( $moduleName ), |
57 | 404 |
58 | ); |
59 | } |
60 | |
61 | return [ |
62 | 'openapi' => '3.0.0', |
63 | 'info' => $this->getInfoSpec( $module ), |
64 | 'servers' => $this->getServerSpec( $module ), |
65 | 'paths' => $this->getPathsSpec( $module ), |
66 | 'components' => $this->getComponentsSpec( $module ), |
67 | ]; |
68 | } |
69 | |
70 | private function getInfoSpec( Module $module ): array { |
71 | // TODO: Let Modules provide their name, description, version, etc |
72 | $prefix = $module->getPathPrefix(); |
73 | |
74 | if ( $prefix === '' ) { |
75 | $title = "Default Module"; |
76 | } else { |
77 | $title = "$prefix Module"; |
78 | } |
79 | |
80 | return [ |
81 | 'title' => $title, |
82 | 'version' => 'undefined', |
83 | 'license' => $this->getLicenseSpec(), |
84 | 'contact' => $this->getContactSpec(), |
85 | ]; |
86 | } |
87 | |
88 | private function getLicenseSpec(): array { |
89 | return [ |
90 | 'name' => $this->options->get( MainConfigNames::RightsText ), |
91 | 'url' => $this->options->get( MainConfigNames::RightsUrl ), |
92 | ]; |
93 | } |
94 | |
95 | private function getContactSpec(): array { |
96 | return [ |
97 | 'email' => $this->options->get( MainConfigNames::EmergencyContact ), |
98 | ]; |
99 | } |
100 | |
101 | private function getServerSpec( Module $module ): array { |
102 | $prefix = $module->getPathPrefix(); |
103 | |
104 | if ( $prefix !== '' ) { |
105 | $prefix = "/$prefix"; |
106 | } |
107 | |
108 | return [ |
109 | [ |
110 | 'url' => $this->getRouter()->getRouteUrl( $prefix ), |
111 | ] |
112 | ]; |
113 | } |
114 | |
115 | private function getPathsSpec( Module $module ): array { |
116 | $specs = []; |
117 | |
118 | foreach ( $module->getDefinedPaths() as $path => $methods ) { |
119 | foreach ( $methods as $mth ) { |
120 | $key = strtolower( $mth ); |
121 | $mth = strtoupper( $mth ); |
122 | $specs[ $path ][ $key ] = $this->getRouteSpec( $module, $path, $mth ); |
123 | } |
124 | } |
125 | |
126 | return $specs; |
127 | } |
128 | |
129 | private function getRouteSpec( Module $module, string $path, string $method ): array { |
130 | $request = new RequestData( [ 'method' => $method ] ); |
131 | $handler = $module->getHandlerForPath( $path, $request, false ); |
132 | |
133 | $operationSpec = $handler->getOpenApiSpec( $method ); |
134 | |
135 | return $operationSpec; |
136 | } |
137 | |
138 | private function getComponentsSpec( Module $module ) { |
139 | $components = []; |
140 | |
141 | // XXX: also collect reusable components from handler specs (but how to avoid name collisions?). |
142 | $componentsSources = [ |
143 | [ 'schemas' => Validator::getParameterTypeSchemas() ], |
144 | ResponseFactory::getResponseComponents() |
145 | ]; |
146 | |
147 | // 2D merge |
148 | foreach ( $componentsSources as $cmps ) { |
149 | foreach ( $cmps as $name => $cmp ) { |
150 | $components[$name] = array_merge( $components[$name] ?? [], $cmp ); |
151 | } |
152 | } |
153 | |
154 | return $components; |
155 | } |
156 | |
157 | public function getParamSettings() { |
158 | return [ |
159 | 'module' => [ |
160 | self::PARAM_SOURCE => 'path', |
161 | ParamValidator::PARAM_TYPE => 'string', |
162 | ParamValidator::PARAM_REQUIRED => true, |
163 | ], |
164 | 'version' => [ |
165 | self::PARAM_SOURCE => 'path', |
166 | ParamValidator::PARAM_TYPE => 'string', |
167 | ParamValidator::PARAM_DEFAULT => '', |
168 | ], |
169 | ]; |
170 | } |
171 | |
172 | } |