Skip to content

Commit

Permalink
Merge pull request #57 from gregcube/api
Browse files Browse the repository at this point in the history
Adds an API to MOT
  • Loading branch information
lehors authored Dec 2, 2024
2 parents ae0cc19 + 47cc3ca commit 933bfa6
Show file tree
Hide file tree
Showing 15 changed files with 655 additions and 52 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ jobs:
--exclude '.github' \
--exclude '.env' \
--exclude 'tests' \
--exclude 'config' \
--exclude 'config/sync' \
--exclude 'models/' \
--exclude 'web/sites/*/files' \
--exclude 'web/libraries' \
Expand All @@ -97,8 +97,9 @@ jobs:
echo "Deploying to $DEPLOY_PATH"
cd $DEPLOY_PATH
composer install --no-dev --no-progress --optimize-autoloader
./vendor/bin/drush cr
./vendor/bin/drush updb -y
./vendor/bin/drush cim --partial --source=modules/mof/config/install -y
./vendor/bin/drush cr
EOF
- name: Revoke IP address
Expand Down
89 changes: 89 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# API

## Overview

This API provides read-only access to a collection of models. It is open to the public and does not require an API key for access.

### Response Format

- All responses are in **JSON** format.

### Rate Limiting

- **Limit**: 10,000 requests per hour per IP address.
- Exceeding this limit will result in temporary rate-limiting.
- Rate-limited responses include the following structure:
```json
{
"error": {
"code": 429,
"message": "Rate limit exceeded. Try again later."
}
}
```

---

## Endpoints

### **GET /api/v1/models**

Lists all models with pagination.

#### Query Parameters:
- `page` (optional): The page number to retrieve (default: `1`). Pages are 1-indexed.
- `limit` (optional): The number of models per page (default: `100`).

#### Example Request:
```http
GET /api/v1/models?page=2&limit=50
```

#### Example Response:
```json
{
"pager": {
"total_items": 235,
"total_pages": 5,
"current_page": 2
},
"models": [...]
}
```

---

### **GET /api/v1/model/{model}**

Retrieves details of a specific model.

#### Path Parameters:
- `model`: The ID of the model to retrieve. Model IDs can be found using the `/api/v1/models` endpoint.

#### Example Request:
```http
GET /api/v1/model/1130
```

#### Example Response:
```json
{
"id": "1130",
"framework": {
"name": "Model Openness Framework",
"version": "1.0",
"date": "2024-12-15"
},
"release": {...},
"classification": {
"class": 1,
"label": "Class I - Open Science",
"progress": {
"1": 100,
"2": 100,
"3": 100
}
}
}
```

17 changes: 17 additions & 0 deletions web/modules/mof/config/install/rest.resource.model_v1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
langcode: en
status: true
dependencies:
module:
- mof
- serialization
- user
id: model_v1
plugin_id: model_v1
granularity: resource
configuration:
methods:
- GET
formats:
- json
authentication:
- cookie
5 changes: 3 additions & 2 deletions web/modules/mof/mof.info.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: 'Model Openness Framework'
type: module
description: '@todo Add description.'
package: '@todo Add package'
description: 'Implements the model openness framework in Drupal'
package: 'Model Openness Tool'
core_version_requirement: ^10
dependencies:
- drupal:options
- drupal:rest
- social_auth_github:social_auth_github
29 changes: 29 additions & 0 deletions web/modules/mof/mof.install
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types=1);

/**
* Implements hook_update_N()
*
* Install dependencies for MOT API.
*/
function mof_update_10201() {
// Enable REST and config management module.
\Drupal::service('module_installer')->install([
'rest',
'config',
'dynamic_page_cache',
'page_cache'
]);

// Enable MOT REST plugin.
\Drupal::service('config.installer')->installDefaultConfig('module', 'mof');

// Add permission to use REST API for anonymous and authenticated users.
foreach (['anonymous', 'authenticated'] as $role_name) {
$role = \Drupal\user\Entity\Role::load($role_name);
if ($role) {
$role->grantPermission('restful get model_v1');
$role->save();
}
}
}

23 changes: 19 additions & 4 deletions web/modules/mof/mof.module
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<?php

declare(strict_types=1);
<?php declare(strict_types=1);

/**
* @file
Expand All @@ -9,8 +7,9 @@ declare(strict_types=1);

use Drupal\Core\Render\Element;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\user\UserInterface;

/**
Expand Down Expand Up @@ -87,3 +86,19 @@ function mof_user_predelete(UserInterface $account): void {
}
}

/**
* Implements hook_query_TAG_alter().
* Adds a model entity access check to model storage queries.
* Ensure the model is approved for non-admin users.
*/
function mof_query_model_access_alter(AlterableInterface $query) {
$user = \Drupal::currentUser();

if (!$user->hasRole('admin') && (int)$user->id() !== 1) {
$base_table = $query->getTables()['base_table']['alias'];
$query->leftJoin('model_field_data', 'mfd', "mfd.id = $base_table.id");
$query->condition('mfd.status', 'approved', '=');
}

return $query;
}
18 changes: 18 additions & 0 deletions web/modules/mof/mof.services.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
services:

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

mof.api.rate_limiter:
class: Drupal\mof\RateLimitMiddleware
arguments:
- '@cache.default'
- '@logger.channel.mot'
tags:
- { name: http_middleware, priority: 400 }

mof.api.exception_subscriber:
class: Drupal\mof\EventSubscriber\ApiExceptionSubscriber
tags:
- { name: event_subscriber }

license_handler:
class: Drupal\mof\LicenseHandler
arguments:
Expand All @@ -24,6 +41,7 @@ services:
- '@serializer'
- '@model_evaluator'
- '@component.manager'
- '@logger.channel.mot'

model_validator:
class: Drupal\mof\ModelValidator
Expand Down
36 changes: 24 additions & 12 deletions web/modules/mof/src/Controller/ModelController.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,19 @@ public function badge(ModelInterface $model, int $class): Response {
* Return a yaml representation of the model.
*/
public function yaml(ModelInterface $model): Response {
$yaml = $this->modelSerializer->toYaml($model);

$response = new Response();
$response->setContent($yaml);
$response->headers->set('Content-Type', 'application/yaml');
$response->headers->set('Content-Length', (string)strlen($yaml));
$response->headers->set('Content-Disposition', 'attachment; filename="mof.yml"');

try {
$yaml = $this->modelSerializer->toYaml($model);
$response->setContent($yaml);
$response->headers->set('Content-Type', 'application/yaml');
$response->headers->set('Content-Length', (string)strlen($yaml));
$response->headers->set('Content-Disposition', 'attachment; filename="mof.yml"');
}
catch (\RuntimeException $e) {
$response->setContent($e->getMessage());
$response->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
}

return $response;
}
Expand All @@ -135,13 +141,19 @@ public function yaml(ModelInterface $model): Response {
* Return a json file representation of the model.
*/
public function json(ModelInterface $model): Response {
$json = $this->modelSerializer->toJson($model);

$response = new Response();
$response->setContent($json);
$response->headers->set('Content-Type', 'application/json');
$response->headers->set('Content-Length', (string)strlen($json));
$response->headers->set('Content-Disposition', 'attachment; filename="mof.json"');

try {
$json = $this->modelSerializer->toJson($model);
$response->setContent($json);
$response->headers->set('Content-Type', 'application/json');
$response->headers->set('Content-Length', (string)strlen($json));
$response->headers->set('Content-Disposition', 'attachment; filename="mof.json"');
}
catch (\RuntimeException $e) {
$response->setContent($e->getMessage());
$response->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
}

return $response;
}
Expand Down
47 changes: 47 additions & 0 deletions web/modules/mof/src/EventSubscriber/ApiExceptionSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php declare(strict_types=1);

namespace Drupal\mof\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\JsonResponse;

/**
* Subscribe to exception events and return JSON responses for API errors.
*/
class ApiExceptionSubscriber implements EventSubscriberInterface {

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [KernelEvents::EXCEPTION => 'onException'];
}

/**
* Handles the exception event.
*
* @param \Symfony\Component\HttpKernel\Event\ExceptionEvent $event
* The exception event.
*/
public function onException(ExceptionEvent $event) {
$request = $event->getRequest();

// Only handle exceptions for API hits.
if (strpos($request->getPathInfo(), '/api') === 0) {
$exception = $event->getThrowable();

$json = [
'error' => [
'code' => $exception->getStatusCode(),
'message' => $exception->getMessage(),
]];

$response = new JsonResponse($json, $exception->getStatusCode());
$event->setResponse($response);
}
}

}

2 changes: 1 addition & 1 deletion web/modules/mof/src/ModelEvaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ private function isOpenSourceLicense(int $cid): bool {
* @return float
* Progress percentage
*/
private function getProgress(int $class): float {
public function getProgress(int $class): float {
$required = $this->getRequiredComponents();
$evaluate = $this->evaluate();

Expand Down
4 changes: 1 addition & 3 deletions web/modules/mof/src/ModelEvaluatorInterface.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<?php

declare(strict_types=1);
<?php declare(strict_types=1);

namespace Drupal\mof;

Expand Down
Loading

0 comments on commit 933bfa6

Please sign in to comment.