OpenAPI Explorer basically works

This commit is contained in:
Andreas Gohr 2023-12-01 20:01:19 +01:00
parent 8a9282a2e6
commit 0c6e917818
3 changed files with 172 additions and 118 deletions

View File

@ -17,10 +17,19 @@ class OpenAPIGenerator
$this->documentation['openapi'] = '3.1.0';
$this->documentation['info'] = [
'title' => 'DokuWiki API',
'description' => 'The DokuWiki API',
'version' => '1.0.0',
'description' => 'The DokuWiki API OpenAPI specification',
'version' => ((string)ApiCore::API_VERSION),
];
$this->documentation['paths'] = [];
}
public function generate()
{
$this->addServers();
$this->addSecurity();
$this->addMethods();
return json_encode($this->documentation, JSON_PRETTY_PRINT);
}
protected function addServers()
@ -32,134 +41,152 @@ class OpenAPIGenerator
];
}
/**
* Parses the description of a method
*
* @param string $desc
* @return array with keys 'summary', 'desc', 'args' and 'return'
*/
protected function parseMethodDescription($desc)
protected function addSecurity()
{
$data = [
'summary' => '',
'desc' => '',
'args' => [],
'return' => '',
$this->documentation['components']['securitySchemes'] = [
'basicAuth' => [
'type' => 'http',
'scheme' => 'basic',
],
'jwt' => [
'type' => 'http',
'scheme' => 'bearer',
'bearerFormat' => 'JWT',
]
];
$lines = explode("\n", trim($desc));
foreach ($lines as $line) {
$line = trim($line);
if ($line && $line[0] === '@') {
// this is a doc block tag
if (str_starts_with('@param', $line)) {
$parts = sexplode(' ', $line, 4); // @param type $name description
$data['args'][] = [ltrim($parts[1], '$'), $parts[3]]; // assumes params are in the right order
continue;
}
if (str_starts_with('@return', $line)) {
$parts = sexplode(' ', $line, 3); // @return type description
$data['return'] = $parts[2];
continue;
}
// ignore all other tags
continue;
}
if (empty($data['summary'])) {
$data['summary'] = $line;
} else {
$data['desc'] .= $line . "\n";
}
}
$data['desc'] = trim($data['desc']);
return $data;
}
protected function getMethodDefinition($method, $info)
{
$desc = $this->parseMethodDescription($info['doc']);
$docs = [
'summary' => $desc['summary'],
'description' => $desc['desc'],
'operationId' => $method,
];
$body = $this->getMethodArguments($info['args'], $desc['args']);
if ($body) $docs['requestBody'] = $body;
return $docs;
}
public function getMethodArguments($args, $info)
{
if (!$args) return null;
$docs = [
'required' => true,
'description' => 'The positional arguments for the method',
'content' => [
'application/json' => [
'schema' => [
'type' => 'array',
'prefixItems' => [],
'unevaluatedItems' => false,
],
],
$this->documentation['security'] = [
[
'basicAuth' => [],
],
[
'jwt' => [],
],
];
foreach ($args as $pos => $type) {
switch ($type) {
case 'int':
$type= 'integer';
break;
case 'bool':
$type = 'boolean';
break;
case 'file':
$type = 'string';
break;
}
$item = [
'type' => $type,
'name' => 'arg' . $pos,
];
if (isset($info[$pos])) {
if (isset($info[$pos][0])) $item['name'] = $info[$pos][0];
if (isset($info[$pos][1])) $item['description'] = $info[$pos][1];
}
$docs['content']['application/json']['schema']['prefixItems'][] = $item;
}
return $docs;
}
protected function addMethods()
{
$methods = $this->api->getMethods();
foreach ($methods as $method => $info) {
$this->documentation['paths'] = [];
foreach ($methods as $method => $call) {
$this->documentation['paths']['/' . $method] = [
'post' => $this->getMethodDefinition($method, $info),
'post' => $this->getMethodDefinition($method, $call),
];
}
}
public function generate()
protected function getMethodDefinition(string $method, ApiCall $call)
{
$this->addServers();
$this->addMethods();
$retType = $this->fixTypes($call->getReturn()['type']);
$retExample = $this->generateExample('result', $retType);
return json_encode($this->documentation, JSON_PRETTY_PRINT);
return [
'operationId' => $method,
'summary' => $call->getSummary(),
'description' => $call->getDescription(),
'requestBody' => [
'required' => true,
'content' => [
'application/json' => $this->getMethodArguments($call->getArgs()),
]
],
'responses' => [
200 => [
'description' => 'Result',
'content' => [
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => [
'result' => [
'type' => $retType,
'description' => $call->getReturn()['description'],
'examples' => [$retExample],
],
'error' => [
'type' => 'object',
'description' => 'Error object in case of an error',
'properties' => [
'code' => [
'type' => 'integer',
'description' => 'The error code',
'examples' => [0],
],
'message' => [
'type' => 'string',
'description' => 'The error message',
'examples' => ['Success'],
],
],
],
],
],
],
],
],
]
];
}
protected function getMethodArguments($args)
{
if (!$args) {
// even if no arguments are needed, we need to define a body
// this is to ensure the openapi spec knows that a application/json header is needed
return ['schema' => ['type' => 'null']];
}
$props = [];
$schema = [
'schema' => [
'type' => 'object',
'properties' => &$props
]
];
foreach ($args as $name => $info) {
$type = $this->fixTypes($info['type']);
$example = $this->generateExample($name, $type);
$props[$name] = [
'type' => $type,
'description' => $info['description'],
'examples' => [ $example ],
];
}
return $schema;
}
protected function fixTypes($type)
{
switch ($type) {
case 'int':
$type = 'integer';
break;
case 'bool':
$type = 'boolean';
break;
case 'file':
$type = 'string';
break;
}
return $type;
}
protected function generateExample($name, $type)
{
switch ($type) {
case 'integer':
return 42;
case 'boolean':
return true;
case 'string':
return 'some-'.$name;
case 'array':
return ['some-'.$name, 'other-'.$name];
default:
return new \stdClass();
}
}
}

View File

View File

@ -1,7 +1,6 @@
<?php
if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/../../');
if (!defined('NOSESSION')) define('NOSESSION', true); // no session or auth required here
require_once(DOKU_INC . 'inc/init.php');
global $INPUT;
@ -17,7 +16,8 @@ if ($INPUT->has('spec')) {
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/openapi-explorer/dist/browser/openapi-explorer.min.js" type="module" defer=""></script>
<script src="https://unpkg.com/openapi-explorer/dist/browser/openapi-explorer.min.js" type="module"
defer=""></script>
<style>
body {
font-family: sans-serif;
@ -28,9 +28,36 @@ if ($INPUT->has('spec')) {
<openapi-explorer
spec-url="<?php echo DOKU_URL ?>lib/exe/openapi.php?spec=1"
hide-server-selection="true"
default-schema-tab="body"
use-path-in-nav-bar="true"
></openapi-explorer>
>
<div slot="overview-api-description">
<p>
This is an auto generated description and OpenAPI specification for the
<a href="https://www.dokuwiki.org/devel/jsonrpc">DokuWiki JSON-RPC API</a>.
It is generated from the source code and the inline documentation.
</p>
<p>
<a href="<?php echo DOKU_BASE ?>/lib/exe/openapi.php?spec=1" download="dokuwiki.json">Download
the API Spec</a>
</p>
</div>
<div slot="authentication-footer">
<p>
<?php
if ($INPUT->server->has('REMOTE_USER')) {
echo 'You are currently logged in as <strong>' . hsc($INPUT->server->str('REMOTE_USER')) . '</strong>.';
echo '<br>API calls in this tool will be automatically executed with your permissions.';
} else {
echo 'You are currently not logged in.<br>';
echo 'You can provide credentials above.';
}
?>
</p>
</div>
</openapi-explorer>
</body>
</html>