AutorÃa | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>.namespace core\router\schema;use coding_exception;use core\router\response\invalid_parameter_response;use core\router\response\not_found_response;use core\router\route;use core\router\route_loader_interface;use core\router\schema\objects\type_base;use core\router\schema\response\response;use core\router\util;use core\url;use stdClass;/*** Moodle OpenApi Specification class.** @package core* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class specification implements\JsonSerializable{/** @var string The OpenAPI version represented in this specification */public const OPENAPI_VERSION = '3.1.0';/** @var stdClass The data which forms the specification */protected stdClass $data;/** @var bool Whether the data has been finalised for output yet */protected bool $finalised = false;/** @var callable[] A list of common responses that are frequently found in paths */protected array $commonresponses = [];/*** Constructor to configure base information.*/public function __construct() {$this->data = (object) ['openapi' => self::OPENAPI_VERSION,'info' => (object) ['title' => 'Moodle LMS','description' => 'REST API for Moodle LMS','summary' => 'Moodle LMS REST API','license' => (object) ['name' => 'GNU GPL v3 or later','url' => 'https://www.gnu.org/licenses/gpl-3.0.html',],],// Servers are added during output.'servers' => [],// Paths are added after initialisation.'paths' => (object) [],'components' => (object) [// Note: This list must be kept in-sync with add_component.'schemas' => (object) [],'responses' => (object) [],'parameters' => (object) [],'examples' => (object) [],'requestBodies' => (object) [],'headers' => (object) [],// The add_component method does not support securitySchemes because we hard-code these.'securitySchemes' => (object) ['api_key' => (object) ['type' => 'apiKey','name' => 'api_key','in' => parameter::IN_HEADER,],'cookie' => (object) ['type' => 'apiKey','name' => 'MoodleSession','in' => parameter::IN_COOKIE,],// TODO MDL-82242: Add support for OAuth2.],],// TODO MDL-82242: Add support for OAuth2.'security' => [(object) ['api_key' => [],'cookie' => [],],],'externalDocs' => (object) ['description' => 'Moodle Developer Docs','url' => 'https://moodledev.io',],];$this->generate_common_responses();}/*** Generate the callables for common responses that are frequently found in paths.** @return specification*/protected function generate_common_responses(): self {$invalidresponse = new invalid_parameter_response();$notfoundresponse = new not_found_response();$this->commonresponses[] = function (route $route,stdClass $data) use ($invalidresponse,$notfoundresponse,): stdClass {if ($route->has_any_validatable_parameter()) {if (!array_key_exists($invalidresponse::get_exception_status_code(), $data->responses)) {$data->responses[$invalidresponse::get_exception_status_code()] = $invalidresponse->get_openapi_schema($this);}if (!array_key_exists($notfoundresponse::get_exception_status_code(), $data->responses)) {$data->responses[$notfoundresponse::get_exception_status_code()] = $notfoundresponse->get_openapi_schema($this);}}return $data;};return $this;}/*** Get the common request responses.** @return callable[]*/public function get_common_request_responses(): array {if (empty($this->commonresponses)) {$this->generate_common_responses(); // @codeCoverageIgnore}return $this->commonresponses;}/*** Finalise the data and prepare it for consumption.*/protected function finalise(): self {global $CFG;if ($this->finalised) {return $this;}// Add the Moodle site version here.$this->data->info->version = $CFG->version;// Add the server configuration.$serverdescription = str_replace("'", "\'", format_string(get_site()->fullname));$this->add_server(url::routed_path(route_loader_interface::ROUTE_GROUP_API)->out(),$serverdescription,);$this->finalised = true;return $this;}/*** Implement the json serialisation interface.** @return mixed*/public function jsonSerialize(): mixed {return $this->get_schema();}/*** Get the OpenAPI schema.** @return stdClass*/final public function get_schema(): stdClass {return $this->finalise()->data;}/*** Add a component to the components object.** https://spec.openapis.org/oas/v3.1.0#components-object** Note: The following component types are supported:** - schemas* - responses* - parameters* - examples* - requestBodies* - headers** At this time, other component types are not supported.** @param openapi_base $object* @return specification* @throws coding_exception If the component type is unknown.*/public function add_component(openapi_base $object): self {match (true) {is_a($object, header_object::class) => $this->add_header($object),is_a($object, parameter::class) => $this->add_parameter($object),is_a($object, response::class) => $this->add_response($object),is_a($object, example::class) => $this->add_example($object),is_a($object, request_body::class) => $this->add_request_body($object),is_a($object, type_base::class) => $this->add_schema($object),default => throw new coding_exception("Unknown object type."),};return $this;}/*** Add a server to the specification.** @param string $url The URL of the API base* @param string $description* @return specification*/public function add_server(string $url,string $description,): self {$this->data->servers[] = (object) ['url' => $url,'description' => $description,];return $this;}/*** Add an API Path.** @param string $component The Moodle component* @param route $route The route which handles this request* @return specification*/public function add_path(string $component,route $route,): self {// Compile the final path, complete with component prefix.$path = "/";$path .= util::normalise_component_path($component);$path .= $route->get_path();// Helper to add the path to the specification.// Note: We use this helper because OpenAPI does not support optional parameters.// Therefore we must handle that in Moodle, adding path variants with and without each optional parameter.$addpath = function (string $path) use ($route, $component) {$path = str_replace([// Remove the optional parameters delimiters from the path.'[',']',// Remove the greedy and non-greedy unlimited delimters from the path too.// These are a FastRoute feature not compatible with OpenAPI.':.*?',':.*',],'',$path,);// Get the OpenAPI description for this path with the updated path.$pathdocs = $this->get_openapi_schema_for_route(route: $route,component: $component,path: $path,);if (!property_exists($this->data->paths, $path)) {$this->data->paths->$path = (object) [];}foreach ((array) $pathdocs as $method => $methoddata) {// Copy each of the pathdocs into place.$this->data->paths->{$path}->{$method} = $methoddata;}};// First add the entire path complete with all optional parameters.// The optional parameter delimiters are `[` and `]`, and are removed in `$addpath`.$addpath($path);// Check for any optional parameters.// OpenAPI does not support optional parameters so we have to duplicate routes instead.// We can determine if this is optional if there is any `[` character before it in the path.// There can be no required parameter after any optional parameter.$optionalparameters = array_filter(array: $route->get_path_parameters(),callback: fn ($parameter) => !$parameter->is_required($route),);if (!empty($optionalparameters)) {// Go through the path from end to start removing optional parameters and adding them to the path list.while (strrpos($path, '[') !== false) {$path = substr($path, 0, strrpos($path, '['));$addpath($path);}}return $this;}/*** Add a schema to the shared components section of the specification.** @param type_base $schema* @return specification*/protected function add_schema(type_base $schema,): self {$name = $schema->get_reference(qualify: false);if (!property_exists($this->data->components->schemas, $name)) {$this->data->components->schemas->$name = $schema->get_openapi_description($this);}return $this;}/*** Add a schema to the shared components section of the specification.** @param parameter $parameter* @return specification*/protected function add_parameter(parameter $parameter,): self {$name = $parameter->get_reference(qualify: false);$this->data->components->parameters->$name = $parameter->get_openapi_description($this);return $this;}/*** Add a header to the shared components section of the specification.** @param header_object $header* @return self*/protected function add_header(header_object $header,): self {$name = $header->get_reference(qualify: false);$this->data->components->headers->$name = $header->get_openapi_description($this);return $this;}/*** Add a response to the shared components section of the specification.** @param response $response* @return specification*/protected function add_response(response $response,): self {$name = $response->get_reference(qualify: false);$this->data->components->responses->$name = $response->get_openapi_description($this);return $this;}/*** Add an example to the shared components section of the specification.** @param example $example* @return specification*/protected function add_example(example $example,): self {$name = $example->get_reference(qualify: false);$this->data->components->examples->$name = $example->get_openapi_description($this);return $this;}/*** Add a request body to the shared components section of the specification.** @param request_body $body* @return specification*/protected function add_request_body(request_body $body,): self {$name = $body->get_reference(qualify: false);$this->data->components->requestBodies->$name = $body->get_openapi_description($this);return $this;}/*** Check whether a reference is defined** @param string $ref* @return bool*/public function is_reference_defined(string $ref,): bool {if (!str_starts_with($ref, '#/components/')) {return false;}// Remove the leading #/components/ part.$ref = substr($ref, strlen('#/components/'));// Split the path and name.[$path, $name] = explode('/', $ref, 2);if (!property_exists($this->data->components, $path)) {return false;}return property_exists($this->data->components->$path, $name);}/*** Get the OpenAPI description for this route.** @param route $route* @param string $component* @param string $path* @return stdClass*/public function get_openapi_schema_for_route(route $route,string $component,string $path,): stdClass {$data = (object) ['description' => $route->description,'summary' => $route->title,'tags' => [$component, ...$route->tags],'parameters' => [],'responses' => [],];if ($route->get_request_body()) {$data->requestBody = $route->get_request_body()->get_openapi_schema(api: $this,path: $path,);}if ($route->security !== null) {$data->security = $route->security;}if ($route->deprecated) {$data->deprecated = true;}foreach ($route->get_responses() as $response) {$data->responses[$response->get_status_code()] = $response->get_openapi_schema(api: $this,path: $path,);}$data->parameters = array_values(array_filter(array_map(fn($param) => $param->get_openapi_schema(api: $this,path: $path,),array_merge($route->get_path_parameters(),$route->get_query_parameters(),$route->get_header_parameters(),),),fn($param) => $param !== null,));foreach ($this->get_common_request_responses() as $callable) {$data = $callable($route, $data);}$methoddata = [];$methods = $route->get_methods(['GET']);foreach ($methods as $method) {$methoddata[strtolower($method)] = $data;}return (object) $methoddata;}}