Skip to content

Commit

Permalink
Merge pull request #2 from Jaesin/8.x-1.x-DRUP-610-459-apidocs-add-url
Browse files Browse the repository at this point in the history
[DRUP-610] Add url as source for apidocs spec file and (DRUP-459) allow re-fetching it afterwards.
  • Loading branch information
cnovak committed May 27, 2019
2 parents 3b82466 + 6f07795 commit 57a5066
Show file tree
Hide file tree
Showing 19 changed files with 821 additions and 54 deletions.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,28 @@ to be able to view published API documentation.

The OpenAPI spec by default is rendered using Apigee SmartDocs.

The OpenAPI spec can be directly uploaded as a file, or associated to a source location
such as Apigee Edge or a URL. A "Re-import OpenAPI spec" operation is available per
API Doc to re-import the spec file when source location changes.

The OpenAPI spec by default is shown on the API Doc detail page by default.
To render the OpenAPI spec using Swagger UI:

1. Install an enable the [Swagger UI Field Formatter](https://www.drupal.org/project/swagger_ui_formatter) module.
2. Install the Swagger UI JS library as documented [on the module page](https://www.drupal.org/project/swagger_ui_formatter).
3. Go to __Configuration > API catalog > Manage display__ in the admin menu.
4. Change "OpenAPI specification" field format to use the Swagger UI field formatter.

The API Doc is an entity, you can configure it at __Configuration > API catalog__ in the admin
menu.

The "APIs" menu link is a view, you can modify it by editing the "API Catalog" view
The "APIs" menu link is a view, you can modify it by editing the "API Catalog" view
under Structure > Views in the admin menu.

## Planned Features

- Integration with Apigee API Products
- Allow OpenAPI specs to be associated to a source location such as Apigee Edge or a URL
- Add visual notifications when source URL specs have changed on the API Doc admin screen
- Ability to update API Docs when source location changes

### Known issues

Expand All @@ -35,10 +45,10 @@ under Structure > Views in the admin menu.

## Installing

This module must be installed on a Drupal site that is managed by Composer. Drupal.org has documentation on how to
This module must be installed on a Drupal site that is managed by Composer. Drupal.org has documentation on how to
[use Composer to manage Drupal site dependencies](https://www.drupal.org/docs/develop/using-composer/using-composer-to-manage-drupal-site-dependencies)
to get you started quickly.

1. Install the module using [Composer](https://getcomposer.org/).
Composer will download the this module and all its dependencies.
**Note**: Composer must be executed at the root of your Drupal installation.
Expand Down
1 change: 1 addition & 0 deletions apigee_api_catalog.info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ configure: entity.apidoc.settings
dependencies:
- drupal:text
- drupal:file
- drupal:file_link
- apigee_edge:apigee_edge
10 changes: 8 additions & 2 deletions apigee_api_catalog.links.task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ entity.apidoc.edit_form:
title: 'Edit'

entity.apidoc.delete_form:
route_name: entity.apidoc.delete_form
base_route: entity.apidoc.canonical
route_name: entity.apidoc.delete_form
base_route: entity.apidoc.canonical
title: 'Delete'
weight: 10

Expand All @@ -31,3 +31,9 @@ entity.apidoc.version_history:
base_route: entity.apidoc.canonical
title: 'Revisions'
weight: 15

entity.apidoc.reimport_spec_form:
route_name: entity.apidoc.reimport_spec_form
base_route: entity.apidoc.canonical
title: 'Re-import OpenAPI spec'
weight: 15
1 change: 0 additions & 1 deletion apigee_api_catalog.module
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,3 @@ function apigee_api_catalog_help($route_name, RouteMatchInterface $route_match)
default:
}
}

5 changes: 5 additions & 0 deletions apigee_api_catalog.services.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
services:

logger.channel.apigee_api_catalog:
parent: logger.channel_base
arguments: ['apigee_api_catalog']

apigee_api_catalog.spec_fetcher:
class: Drupal\apigee_api_catalog\SpecFetcher
arguments: ['@file_system', '@http_client', '@entity_type.manager', '@string_translation', '@messenger', '@logger.channel.apigee_api_catalog']
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"description": "Apigee API Catalog for Drupal",
"require": {
"php": ">=7.1",
"drupal/apigee_edge": "~1.0-rc4"
"drupal/apigee_edge": "~1.0-rc4",
"drupal/file_link": "~1.0"
},
"require-dev": {
"drupal/coder": "^8.3",
Expand Down
49 changes: 26 additions & 23 deletions src/Entity/Access/ApiDocAccessControlHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

namespace Drupal\apigee_api_catalog\Entity\Access;

use Drupal\apigee_api_catalog\Entity\ApiDocInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
Expand Down Expand Up @@ -70,32 +71,34 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
$parent_access = parent::checkAccess($entity, $operation, $account);

if (!$parent_access->isAllowed()) {
/** @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */

// Access control for revisions.
if (!$entity->isDefaultRevision()) {
return $this->checkAccessRevisions($entity, $operation, $account);
}
switch ($operation) {
case 'view':
if (!$entity->isPublished()) {
return $parent_access->orIf(AccessResult::allowedIfHasPermission($account, 'view unpublished apidoc entities'));
}
return $parent_access->orIf(AccessResult::allowedIfHasPermission($account, 'view published apidoc entities'));

case 'update':
return $parent_access->orIf(AccessResult::allowedIfHasPermission($account, 'edit apidoc entities'));

case 'delete':
return $parent_access->orIf(AccessResult::allowedIfHasPermission($account, 'delete apidoc entities'));
}
/** @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */
$access = parent::checkAccess($entity, $operation, $account);

// Access control for revisions.
if (!$entity->isDefaultRevision()) {
return $this->checkAccessRevisions($entity, $operation, $account);
}

switch ($operation) {
case 'view':
return $access->orIf($entity->isPublished()
? AccessResult::allowedIfHasPermission($account, 'view published apidoc entities')
: AccessResult::allowedIfHasPermission($account, 'view unpublished apidoc entities')
);

case 'reimport':
return AccessResult::allowedIf($entity->spec_file_source->value === ApiDocInterface::SPEC_AS_URL)
->andIf($entity->access('update', $account, TRUE));

case 'update':
return $access->orIf(AccessResult::allowedIfHasPermission($account, 'edit apidoc entities'));

case 'delete':
return $access->orIf(AccessResult::allowedIfHasPermission($account, 'delete apidoc entities'));
}

// Unknown operation, no opinion.
return $parent_access;
return $access;
}

/**
Expand Down
89 changes: 75 additions & 14 deletions src/Entity/ApiDoc.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\link\LinkItemInterface;

/**
* Defines the API Doc entity.
Expand All @@ -45,6 +46,7 @@
* "add" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocForm",
* "edit" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocForm",
* "delete" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocDeleteForm",
* "reimport_spec" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocReimportSpecForm",
* },
* "access" = "Drupal\apigee_api_catalog\Entity\Access\ApiDocAccessControlHandler",
* "route_provider" = {
Expand Down Expand Up @@ -77,6 +79,7 @@
* "add-form" = "/admin/content/api/add",
* "edit-form" = "/admin/content/api/{apidoc}/edit",
* "delete-form" = "/admin/content/api/{apidoc}/delete",
* "reimport-spec-form" = "/admin/content/api/{apidoc}/reimport",
* "version-history" = "/admin/content/api/{apidoc}/revisions",
* "revision" = "/admin/content/api/{apidoc}/revisions/{apidoc_revision}/view",
* "revision-revert-form" = "/admin/content/api/{apidoc}/revisions/{apidoc_revision}/revert",
Expand All @@ -92,44 +95,44 @@ class ApiDoc extends EditorialContentEntityBase implements ApiDocInterface {
/**
* {@inheritdoc}
*/
public function getName() : string {
public function getName(): string {
return $this->get('name')->value;
}

/**
* {@inheritdoc}
*/
public function setName(string $name) : ApiDocInterface {
public function setName(string $name): ApiDocInterface {
$this->set('name', $name);
return $this;
}

/**
* {@inheritdoc}
*/
public function getDescription() : string {
public function getDescription(): string {
return $this->get('description')->value;
}

/**
* {@inheritdoc}
*/
public function setDescription(string $description) : ApiDocInterface {
public function setDescription(string $description): ApiDocInterface {
$this->set('description', $description);
return $this;
}

/**
* {@inheritdoc}
*/
public function getCreatedTime() : int {
public function getCreatedTime(): int {
return $this->get('created')->value;
}

/**
* {@inheritdoc}
*/
public function setCreatedTime(int $timestamp) : ApiDocInterface {
public function setCreatedTime(int $timestamp): ApiDocInterface {
$this->set('created', $timestamp);
return $this;
}
Expand All @@ -145,6 +148,23 @@ public function preSave(EntityStorageInterface $storage) {
if (!$this->getRevisionUser()) {
$this->setRevisionUserId(\Drupal::currentUser()->id());
}

\Drupal::service('apigee_api_catalog.spec_fetcher')->fetchSpec($this, FALSE, FALSE);

// API docs that use the "file" source will still need their md5 updated.
if ($this->get('spec_file_source')->value === static::SPEC_AS_FILE) {
$spec_value = $this->get('spec')->isEmpty() ? [] : $this->get('spec')->getValue()[0];
if (!empty($spec_value['target_id'])) {
/* @var \Drupal\file\Entity\File $file */
$file = $this->entityTypeManager()
->getStorage('file')
->load($spec_value['target_id']);

if ($file) {
$this->set('spec_md5', md5_file($file->getFileUri()));
}
}
}
}

/**
Expand Down Expand Up @@ -218,8 +238,25 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
])
->setDisplayConfigurable('form', TRUE);

$fields['spec_file_source'] = BaseFieldDefinition::create('list_string')
->setLabel(t('OpenAPI specification file source'))
->setDescription(t('Indicate if the OpenAPI spec will be provided as a
file for upload or a URL.'))
->setDefaultValue(ApiDocInterface::SPEC_AS_FILE)
->setRequired(TRUE)
->setSetting('allowed_values', [
ApiDocInterface::SPEC_AS_FILE => t('File'),
ApiDocInterface::SPEC_AS_URL => t('URL'),
])
->setDisplayOptions('form', [
'type' => 'options_buttons',
'weight' => 0,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);

$fields['spec'] = BaseFieldDefinition::create('file')
->setLabel(t('OpenAPI specification'))
->setLabel(t('OpenAPI specification file'))
->setDescription(t('The spec snapshot.'))
->setRevisionable(TRUE)
->setSettings([
Expand All @@ -240,22 +277,42 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
->setDisplayOptions('form', [
'label' => 'hidden',
'type' => 'file_generic',
])
->setDisplayConfigurable('form', FALSE)
->setDisplayConfigurable('view', FALSE);

$fields['api_product'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('API Product'))
->setDescription(t('The API Product this API is associated with.'))
->setRevisionable(TRUE)
->setSetting('target_type', 'api_product')
$fields['file_link'] = BaseFieldDefinition::create('file_link')
->setLabel(t('URL to OpenAPI specification file'))
->setDescription(t('The URL to an OpenAPI file spec.'))
->addConstraint('ApiDocFileLink')
->setSettings([
'file_extensions' => 'yml yaml json',
'link_type' => LinkItemInterface::LINK_GENERIC,
'title' => DRUPAL_DISABLED,
])
->setDisplayOptions('form', [
'label' => 'above',
'type' => 'entity_reference_autocomplete',
'weight' => 0,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);

$fields['spec_md5'] = BaseFieldDefinition::create('string')
->setLabel(t('OpenAPI specification file MD5'))
->setDescription(t('OpenAPI specification file MD5'))
->setSettings([
'text_processing' => 0,
])
->setDisplayConfigurable('form', FALSE)
->setDisplayConfigurable('view', FALSE);

$fields['api_product'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('API Product'))
->setDescription(t('The API Product this API is associated with.'))
->setRevisionable(TRUE)
->setSetting('target_type', 'api_product')
->setDisplayConfigurable('form', FALSE)
->setDisplayConfigurable('view', FALSE);

$fields['status']
->setLabel(t('Publishing status'))
->setDescription(t('A boolean indicating whether the API Doc is published.'))
Expand All @@ -274,6 +331,10 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
->setDescription(t('The time that the entity was last edited.'))
->setRevisionable(TRUE);

$fields['fetched_timestamp'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Spec fetched from URL timestamp'))
->setDescription(t('When the OpenAPI spec file was last fetched from URL as a Unix timestamp.'));

return $fields;
}

Expand Down

0 comments on commit 57a5066

Please sign in to comment.