definitionFile = $definitionFile; } /** * Get a config version hash for cache invalidation * * @return string */ protected function getConfigHash(): string { if ( $this->configHash === null ) { $this->configHash = md5( json_encode( [ 'class' => __CLASS__, 'version' => 1, 'fileTimestamps' => $this->getRouteFileTimestamp() ] ) ); } return $this->configHash; } /** * Load the module definition file. * * @return array */ private function getModuleDefinition(): array { if ( $this->moduleDef !== null ) { return $this->moduleDef; } $this->routeFileTimestamp = filemtime( $this->definitionFile ); $moduleDef = $this->loadJsonFile( $this->definitionFile ); if ( !$moduleDef ) { throw new ModuleConfigurationException( 'Malformed module definition file: ' . $this->definitionFile ); } if ( !isset( $moduleDef['mwapi'] ) ) { throw new ModuleConfigurationException( 'Missing mwapi version field in ' . $this->definitionFile ); } // Require OpenAPI version 3.1 or compatible. if ( !version_compare( $moduleDef['mwapi'], '1.0.999', '<=' ) || !version_compare( $moduleDef['mwapi'], '1.0.0', '>=' ) ) { throw new ModuleConfigurationException( "Unsupported openapi version {$moduleDef['mwapi']} in " . $this->definitionFile ); } $this->moduleDef = $moduleDef; return $this->moduleDef; } /** * Get last modification times of the module definition file. */ private function getRouteFileTimestamp(): int { if ( $this->routeFileTimestamp === null ) { $this->routeFileTimestamp = filemtime( $this->definitionFile ); } return $this->routeFileTimestamp; } /** * @unstable for testing * * @return array[] */ public function getDefinedPaths(): array { $paths = []; $moduleDef = $this->getModuleDefinition(); foreach ( $moduleDef['paths'] as $path => $pSpec ) { $paths[$path] = []; foreach ( $pSpec as $method => $opSpec ) { $paths[$path][] = strtoupper( $method ); } } return $paths; } protected function initRoutes(): void { $moduleDef = $this->getModuleDefinition(); // The structure is similar to OpenAPI, see docs/rest/mwapi.1.0.json foreach ( $moduleDef['paths'] as $path => $pathSpec ) { foreach ( $pathSpec as $method => $opSpec ) { $info = $this->makeRouteInfo( $path, $opSpec ); $this->addRoute( $method, $path, $info ); } } } /** * Generate a route info array to be stored in the matcher tree, * in the form expected by MatcherBasedModule::addRoute() * and ultimately Module::getHandlerForPath(). */ private function makeRouteInfo( string $path, array $opSpec ): array { static $objectSpecKeys = [ 'class', 'factory', 'services', 'optional_services', 'args', ]; static $oasKeys = [ 'parameters', 'responses', 'summary', 'description', 'tags', 'externalDocs', ]; if ( isset( $opSpec['redirect'] ) ) { // Redirect shorthand $opSpec['handler'] = [ 'class' => RedirectHandler::class, 'redirect' => $opSpec['redirect'], ]; unset( $opSpec['redirect'] ); } $handlerSpec = $opSpec['handler'] ?? null; if ( !$handlerSpec ) { throw new RouteDefinitionException( 'Missing handler spec' ); } $info = [ 'spec' => array_intersect_key( $handlerSpec, array_flip( $objectSpecKeys ) ), 'config' => array_diff_key( $handlerSpec, array_flip( $objectSpecKeys ) ), 'OAS' => array_intersect_key( $opSpec, array_flip( $oasKeys ) ), 'path' => $path, ]; return $info; } public function getOpenApiInfo() { $def = $this->getModuleDefinition(); return $def['info'] ?? []; } }