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