openapi_rest-8.x-2.0-rc2/src/Plugin/openapi/OpenApiGenerator/RestGenerator.php
src/Plugin/openapi/OpenApiGenerator/RestGenerator.php
<?php
namespace Drupal\openapi_rest\Plugin\openapi\OpenApiGenerator;
use Drupal\Core\Authentication\AuthenticationCollectorInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\openapi\Plugin\openapi\OpenApiGeneratorBase;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\openapi_rest\RestInspectionTrait;
use Drupal\rest\RestResourceConfigInterface;
use Drupal\schemata\SchemaFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Route;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Defines an OpenApi Schema Generator for the Rest module.
*
* @OpenApiGenerator(
* id = "rest",
* label = @Translation("REST"),
* )
*/
class RestGenerator extends OpenApiGeneratorBase {
use RestInspectionTrait;
/**
* The Schemata SchemaFactory.
*
* @var \Drupal\schemata\SchemaFactory
*/
protected $schemaFactory;
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RouteProviderInterface $routing_provider, EntityFieldManagerInterface $field_manager, SerializerInterface $serializer, RequestStack $request_stack, ConfigFactoryInterface $config_factory, AuthenticationCollectorInterface $authentication_collector, SchemaFactory $schema_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $routing_provider, $field_manager, $serializer, $request_stack, $config_factory, $authentication_collector);
$this->schemaFactory = $schema_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('router.route_provider'),
$container->get('entity_field.manager'),
$container->get('serializer'),
$container->get('request_stack'),
$container->get('config.factory'),
$container->get('authentication_collector'),
$container->get('schemata.schema_factory')
);
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
$bundle_name = isset($this->getOptions()['bundle_name']) ? $this->getOptions()['bundle_name'] : NULL;
$entity_type_id = isset($this->getOptions()['entity_type_id']) ? $this->getOptions()['entity_type_id'] : NULL;
static $definitions = [];
if (!$definitions) {
$entity_types = $this->getRestEnabledEntityTypes($entity_type_id);
$definitions = [];
foreach ($entity_types as $entity_id => $entity_type) {
$entity_schema = $this->getJsonSchema('json', $entity_id);
$definitions[$entity_id] = $entity_schema;
if ($bundle_type = $entity_type->getBundleEntityType()) {
$bundle_storage = $this->entityTypeManager->getStorage($bundle_type);
if ($bundle_name) {
$bundles[$bundle_name] = $bundle_storage->load($bundle_name);
}
else {
$bundles = $bundle_storage->loadMultiple();
}
foreach ($bundles as $bundle => $bundle_data) {
$bundle_schema = $this->getJsonSchema('json', $entity_id, $bundle);
foreach ($entity_schema['properties'] as $property_id => $property) {
if (isset($bundle_schema['properties'][$property_id]) && $bundle_schema['properties'][$property_id] === $property) {
// Remove any bundle schema property that is the same as the
// entity schema property.
unset($bundle_schema['properties'][$property_id]);
}
}
// Use Open API polymorphism support to show that bundles extend
// entity type.
// @todo Should base fields be removed from bundle schema?
// @todo Can base fields could be different from entity type base fields?
// @see hook_entity_bundle_field_info().
// @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#models-with-polymorphism-support
$definitions[$this->getEntityDefinitionKey($entity_type->id(), $bundle)] = [
'allOf' => [
['$ref' => "#/definitions/$entity_id"],
$bundle_schema,
],
];
}
}
}
}
return $definitions;
}
/**
* {@inheritdoc}
*/
protected function getJsonSchema($described_format, $entity_type_id, $bundle_name = NULL) {
if ($entity_type_id !== $bundle_name) {
$schema = $this->schemaFactory->create($entity_type_id, $bundle_name);
}
else {
$schema = $this->schemaFactory->create($entity_type_id);
}
if ($schema) {
$json_schema = $this->serializer->normalize($schema, "schema_json:$described_format");
unset($json_schema['$schema'], $json_schema['id']);
$json_schema = $this->cleanSchema($json_schema);
if (!$bundle_name) {
// Add discriminator field.
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if ($bundle_field = $entity_type->getKey('bundle')) {
$json_schema['discriminator'] = $bundle_field;
}
}
}
else {
$json_schema = [
'type' => 'object',
'title' => $this->t('@entity_type Schema', ['@entity_type' => $entity_type_id]),
'description' => $this->t('Describes the payload for @entity_type entities.', ['@entity_type' => $entity_type_id]),
];
}
return $json_schema;
}
/**
* {@inheritdoc}
*/
public function getConsumes() {
return $this->generateMimeTypesFromFormats($this->getRestSupportedFormats());
}
/**
* {@inheritdoc}
*/
public function getProduces() {
return $this->generateMimeTypesFromFormats($this->getRestSupportedFormats());
}
/**
* Get tags.
*/
public function getTags() {
$entity_types = $this->getRestEnabledEntityTypes();
$tags = [];
foreach ($entity_types as $entity_type) {
if ($this->includeEntityTypeBundle($entity_type->id())) {
$tag = [
'name' => $entity_type->id(),
'description' => $this->t("Entity type: @label", ['@label' => $entity_type->getLabel()]),
'x-entity-type' => $entity_type->id(),
'x-definition' => [
'$ref' => '#/definitions/' . $this->getEntityDefinitionKey($entity_type->id()),
],
];
$tags[] = $tag;
}
}
return $tags;
}
/**
* {@inheritdoc}
*/
public function getPaths() {
$bundle_name = isset($this->getOptions()['bundle_name']) ? $this->getOptions()['bundle_name'] : NULL;
$resource_configs = $this->getResourceConfigs($this->getOptions());
if (!$resource_configs) {
return [];
}
$api_paths = [];
foreach ($resource_configs as $resource_config) {
/** @var \Drupal\rest\Plugin\ResourceBase $plugin */
$resource_plugin = $resource_config->getResourcePlugin();
foreach ($resource_config->getMethods() as $method) {
if ($route = $this->getRouteForResourceMethod($resource_config, $method)) {
$open_api_method = strtolower($method);
$path = $route->getPath();
$path_method_spec = [];
$formats = $this->getMethodSupportedFormats($method, $resource_config);
$format_parameter = [
'name' => '_format',
'in' => 'query',
'type' => 'string',
'enum' => $formats,
'required' => TRUE,
'description' => 'Request format',
];
if (count($formats) == 1) {
$format_parameter['default'] = $formats[0];
}
$path_method_spec['parameters'][] = $format_parameter;
$path_method_spec['responses'] = $this->getErrorResponses();
if ($this->isEntityResource($resource_config)) {
$entity_type = $this->getEntityType($resource_config);
$path_method_spec['tags'] = [$entity_type->id()];
$path_method_spec['summary'] = $this->t('@method a @entity_type', [
'@method' => ucfirst($open_api_method),
'@entity_type' => $entity_type->getLabel(),
]);
$path_method_spec['parameters'] = array_merge($path_method_spec['parameters'], $this->getEntityParameters($entity_type, $method, $bundle_name));
$path_method_spec['responses'] = $this->getEntityResponses($entity_type->id(), $method, $bundle_name) + $path_method_spec['responses'];
}
else {
$path_method_spec['responses']['200'] = [
'description' => 'successful operation',
];
$path_method_spec['summary'] = $resource_plugin->getPluginDefinition()['label'];
$path_method_spec['parameters'] = array_merge($path_method_spec['parameters'], $this->getRouteParameters($route));
}
$path_method_spec['operationId'] = $resource_plugin->getPluginId() . ":" . $method;
$path_method_spec['schemes'] = [$this->request->getScheme()];
$path_method_spec['security'] = $this->getResourceSecurity($resource_config, $method, $formats);
$api_paths[$path][$open_api_method] = $path_method_spec;
}
}
}
return $api_paths;
}
/**
* Gets the matching for route for the resource and method.
*
* @param \Drupal\rest\RestResourceConfigInterface $resource_config
* The REST config resource.
* @param string $method
* The HTTP method.
*
* @return \Symfony\Component\Routing\Route
* The route.
*
* @throws \Exception
* If no route is found.
*/
protected function getRouteForResourceMethod(RestResourceConfigInterface $resource_config, $method) {
if ($this->isEntityResource($resource_config)) {
$route_name = 'rest.' . $resource_config->id() . ".$method";
$routes = $this->routingProvider->getRoutesByNames([$route_name]);
if (empty($routes)) {
$formats = $resource_config->getFormats($method);
if (count($formats) > 0) {
$route_name .= ".{$formats[0]}";
$routes = $this->routingProvider->getRoutesByNames([$route_name]);
}
}
if ($routes) {
return array_pop($routes);
}
}
else {
$resource_plugin = $resource_config->getResourcePlugin();
foreach ($resource_plugin->routes() as $route) {
$methods = $route->getMethods();
if (array_search($method, $methods) !== FALSE) {
return $route;
}
};
}
throw new \Exception("No route found for REST resource, {$resource_config->id()}, for method $method");
}
/**
* Get the error responses.
*
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#responseObject
*
* @return array
* Keys are http codes. Values responses.
*/
protected function getErrorResponses() {
$responses['400'] = [
'description' => 'Bad request',
'schema' => [
'type' => 'object',
'properties' => [
'error' => [
'type' => 'string',
'example' => 'Bad data',
],
],
],
];
$responses['500'] = [
'description' => 'Internal server error.',
'schema' => [
'type' => 'object',
'properties' => [
'message' => [
'type' => 'string',
'example' => 'Internal server error.',
],
],
],
];
return $responses;
}
/**
* Get parameters for an entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param string $method
* The HTTP method.
* @param string $bundle_name
* The bundle name.
*
* @return array
* Parameters for the entity resource.
*/
protected function getEntityParameters(EntityTypeInterface $entity_type, $method, $bundle_name = NULL) {
$parameters = [];
if (in_array($method, ['GET', 'DELETE', 'PATCH'])) {
$keys = $entity_type->getKeys();
if ($entity_type instanceof ConfigEntityTypeInterface) {
$key_type = 'string';
}
else {
if ($entity_type instanceof FieldableEntityInterface) {
$key_field = $this->fieldManager->getFieldStorageDefinitions($entity_type->id())[$keys['id']];
$key_type = $key_field->getType();
}
else {
$key_type = 'string';
}
}
$parameters[] = [
'name' => $entity_type->id(),
'in' => 'path',
'required' => TRUE,
'type' => $key_type,
'description' => $this->t('The @id,id, of the @type.', [
'@id' => $keys['id'],
'@type' => $entity_type->id(),
]),
];
}
if (in_array($method, ['POST', 'PATCH'])) {
$parameters[] = [
'name' => 'body',
'in' => 'body',
'description' => $this->t('The @label object', ['@label' => $entity_type->getLabel()]),
'required' => TRUE,
'schema' => [
'$ref' => '#/definitions/' . $this->getEntityDefinitionKey($entity_type->id(), $bundle_name),
],
];
}
return $parameters;
}
/**
* Get OpenAPI parameters for a route.
*
* @param \Symfony\Component\Routing\Route $route
* The route.
*
* @return array
* The resource parameters.
*/
protected function getRouteParameters(Route $route) {
$parameters = [];
$vars = $route->compile()->getPathVariables();
foreach ($vars as $var) {
$parameters[] = [
'name' => $var,
'type' => 'string',
'in' => 'path',
'required' => TRUE,
];
}
return $parameters;
}
/**
* Get the security information for the a resource.
*
* @param \Drupal\rest\RestResourceConfigInterface $resource_config
* The REST resource.
* @param string $method
* The HTTP method.
* @param string[] $formats
* The formats.
*
* @return array
* The security elements.
*
* @see http://swagger.io/specification/#securityDefinitionsObject
*/
public function getResourceSecurity(RestResourceConfigInterface $resource_config, $method, array $formats) {
$security = [];
foreach ($resource_config->getAuthenticationProviders($method) as $auth) {
switch ($auth) {
case 'basic_auth':
case 'cookie':
case 'oauth':
case 'oauth2':
// @TODO: #2977109 - Calculate oauth scopes required.
$security[] = [$auth => []];
break;
}
}
// @todo Handle tokens that need to be set in headers.
if ($this->isEntityResource($resource_config)) {
$route_name = 'rest.' . $resource_config->id() . ".$method";
$routes = $this->routingProvider->getRoutesByNames([$route_name]);
if (empty($routes) && count($formats) > 1) {
$route_name .= ".{$formats[0]}";
$routes = $this->routingProvider->getRoutesByNames([$route_name]);
}
if ($routes) {
$route = array_pop($routes);
// Check to see if route is protected by access checks in header.
if ($route->getRequirement('_csrf_request_header_token')) {
$security[] = ['csrf_token' => []];
}
}
}
return $security;
}
/**
* {@inheritdoc}
*/
public function getApiName() {
return $this->t('REST API');
}
/**
* {@inheritdoc}
*/
protected function getApiDescription() {
return $this->t('The REST API provided by the core REST module.');
}
/**
* List of supported Serializer Formats for HTTP Method on a Rest Resource.
*
* @param string $method
* The HTTP method for generating the MIME Types. Example: GET, POST, etc.
* @param \Drupal\rest\RestResourceConfigInterface $resource_config
* The resource configuration.
*
* @return array
* The list of MIME Types
*/
protected function getMethodSupportedFormats($method, RestResourceConfigInterface $resource_config) {
if (empty($method)) {
return [];
}
// The route ID.
$route_id = "rest.{$resource_config->id()}.$method";
// First Check the supported formats on the route level.
/** @var \Symfony\Component\Routing\Route[] $route */
$routes = $this->routingProvider->getRoutesByNames([$route_id]);
if (!empty($routes) && array_key_exists($route_id, $routes) && $formats = $routes[$route_id]->getRequirement('_format')) {
return explode('|', $formats);
}
// If no route level format was found, lets use
// the RestResourceConfig formats for the given HTTP method.
return $resource_config->getFormats($method);
}
/**
* Generate list of MIME Types based on a list of serializer formats.
*
* @param array $formats
* List of formats.
*
* @return array
* List of MIME Types based on $formats.
* The list is MIME Types are on the same order as the inserted $format
*/
protected function generateMimeTypesFromFormats(array $formats) {
$mime_types = [];
foreach ($formats as $format) {
$mime_types[] = 'application/' . preg_replace('/_/', '+', trim(strtolower($format)));
}
return $mime_types;
}
/**
* Returns a list of supported Format on REST.
*
* @return array
* The list of supported formats.
*/
protected function getRestSupportedFormats() {
static $supported_formats = [];
if (empty($supported_formats)) {
$resource_configs = $this->getResourceConfigs($this->getOptions());
if (empty($resource_configs)) {
return [];
}
foreach ($resource_configs as $resource_config) {
foreach ($resource_config->getMethods() as $method) {
$formats = $this->getMethodSupportedFormats($method, $resource_config);
$supported_formats = array_unique(array_merge($supported_formats, $formats), SORT_REGULAR);
}
}
}
return $supported_formats;
}
}
