OpenAPI Generator. Better DocBlock parsing [WIP]
This introduces a new DocBlock parser to properly generate API specifications. It also introduces the concept of Response classes to better specify the response format. This is still very much in progress.
This commit is contained in:
parent
fe9f11e2d0
commit
8ddd9b6918
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\test\Remote\OpenApiDoc;
|
||||
|
||||
use dokuwiki\Remote\OpenApiDoc\ClassResolver;
|
||||
|
||||
class ClassResolverTest extends \DokuWikiTest
|
||||
{
|
||||
|
||||
|
||||
public function testResolving()
|
||||
{
|
||||
$resolver = new ClassResolver();
|
||||
|
||||
// resolve by use statement
|
||||
$this->assertEquals(ClassResolver::class, $resolver->resolve('ClassResolver', self::class));
|
||||
|
||||
// resolve in same namespace
|
||||
$this->assertEquals(
|
||||
'dokuwiki\test\Remote\OpenApiDoc\Something\Else',
|
||||
$resolver->resolve('Something\Else', self::class)
|
||||
);
|
||||
|
||||
// resolve fully qualified
|
||||
$this->assertEquals(
|
||||
'fully\Qualified\Class',
|
||||
$resolver->resolve('\fully\Qualified\Class', self::class)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\test\Remote\OpenApiDoc;
|
||||
|
||||
use dokuwiki\Remote\OpenApiDoc\DocBlockClass;
|
||||
use dokuwiki\Remote\OpenApiDoc\DocBlockMethod;
|
||||
use dokuwiki\Remote\OpenApiDoc\DocBlockProperty;
|
||||
|
||||
/**
|
||||
* Test cases for DocBlockClass
|
||||
*
|
||||
* This test class is also used in the tests itself
|
||||
*/
|
||||
class DocBlockClassTest extends \DokuWikiTest
|
||||
{
|
||||
/** @var string This is a dummy */
|
||||
public $dummyProperty1 = 'dummy';
|
||||
|
||||
/**
|
||||
* Parse this test class with the DocBlockClass
|
||||
*
|
||||
* Also tests property and method access
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testClass()
|
||||
{
|
||||
$reflect = new \ReflectionClass($this);
|
||||
$doc = new DocBlockClass($reflect);
|
||||
|
||||
$this->assertStringContainsString('Test cases for DocBlockClass', $doc->getSummary());
|
||||
$this->assertStringContainsString('used in the tests itself', $doc->getDescription());
|
||||
|
||||
$this->assertInstanceOf(DocBlockProperty::class, $doc->getPropertyDocs()['dummyProperty1']);
|
||||
$this->assertEquals('This is a dummy', $doc->getPropertyDocs()['dummyProperty1']->getSummary());
|
||||
|
||||
$this->assertInstanceOf(DocBlockMethod::class, $doc->getMethodDocs()['testClass']);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\test\Remote\OpenApiDoc;
|
||||
|
||||
use dokuwiki\Remote\OpenApiDoc\DocBlockMethod;
|
||||
|
||||
class DocBlockMethodTest extends \DokuWikiTest {
|
||||
|
||||
|
||||
/**
|
||||
* This is a test
|
||||
*
|
||||
* With more information
|
||||
* in several lines
|
||||
* @param string $foo First variable
|
||||
* @param int $bar
|
||||
* @param string[] $baz
|
||||
* @something else
|
||||
* @something other
|
||||
* @another tag
|
||||
* @return string The return
|
||||
*/
|
||||
public function dummyMethod1($foo, $bar, $baz=['a default'])
|
||||
{
|
||||
return 'dummy';
|
||||
}
|
||||
|
||||
public function testMethod()
|
||||
{
|
||||
$reflect = new \ReflectionMethod($this, 'dummyMethod1');
|
||||
$doc = new DocBlockMethod($reflect);
|
||||
|
||||
$this->assertEquals('This is a test', $doc->getSummary());
|
||||
$this->assertEquals("With more information\nin several lines", $doc->getDescription());
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'foo' => [
|
||||
'type' => 'string',
|
||||
'description' => 'First variable',
|
||||
'optional' => false,
|
||||
],
|
||||
'bar' => [
|
||||
'type' => 'int',
|
||||
'description' => '',
|
||||
'optional' => false,
|
||||
],
|
||||
'baz' => [
|
||||
'type' => 'string[]',
|
||||
'description' => '',
|
||||
'optional' => true,
|
||||
'default' => ['a default'],
|
||||
],
|
||||
],
|
||||
$doc->getTag('param')
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'type' => 'string',
|
||||
'description' => 'The return'
|
||||
],
|
||||
$doc->getTag('return')
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'else',
|
||||
'other',
|
||||
],
|
||||
$doc->getTag('something')
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'tag',
|
||||
],
|
||||
$doc->getTag('another')
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\test\Remote\OpenApiDoc;
|
||||
|
||||
use dokuwiki\Remote\OpenApiDoc\Type;
|
||||
|
||||
class TypeTest extends \DokuWikiTest
|
||||
{
|
||||
|
||||
public function provideBaseTypes()
|
||||
{
|
||||
return [
|
||||
['string', 'string', 'string'],
|
||||
['string', 'string', 'string', self::class],
|
||||
['int', 'int', 'int'],
|
||||
['int', 'int', 'int', self::class],
|
||||
['file', 'string', 'file'],
|
||||
['file', 'string', 'file', self::class],
|
||||
['date', 'int', 'date'],
|
||||
['date', 'int', 'date', self::class],
|
||||
['boolean', 'bool', 'bool'],
|
||||
['boolean', 'bool', 'bool', self::class],
|
||||
['false', 'bool', 'bool'],
|
||||
['false', 'bool', 'bool', self::class],
|
||||
['true', 'bool', 'bool'],
|
||||
['true', 'bool', 'bool', self::class],
|
||||
['integer', 'int', 'int'],
|
||||
['integer', 'int', 'int', self::class],
|
||||
['array', 'array', 'array'],
|
||||
['array', 'array', 'array', self::class],
|
||||
['array[]', 'array', 'array'],
|
||||
['array[]', 'array', 'array', self::class],
|
||||
['foo', 'foo', 'object'],
|
||||
['foo', 'foo', 'object', self::class],
|
||||
['foo[]', 'array', 'array'],
|
||||
['foo[]', 'array', 'array', self::class],
|
||||
['Foo', 'Foo', 'object'],
|
||||
['Foo', 'dokuwiki\\test\\Remote\\OpenApiDoc\\Foo', 'object', self::class],
|
||||
['\\Foo', 'Foo', 'object'],
|
||||
['\\Foo', 'Foo', 'object', self::class],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideBaseTypes
|
||||
* @param $typehint
|
||||
* @param $expectedJSONRPCType
|
||||
* @param $expectedXMLRPCType
|
||||
* @param $context
|
||||
* @return void
|
||||
*/
|
||||
public function testJSONBaseTypes($typehint, $expectedJSONRPCType, $expectedXMLRPCType, $context = '')
|
||||
{
|
||||
$type = new Type($typehint, $context);
|
||||
$this->assertEquals($expectedJSONRPCType, $type->getJSONRPCType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideBaseTypes
|
||||
* @param $typehint
|
||||
* @param $expectedJSONRPCType
|
||||
* @param $expectedXMLRPCType
|
||||
* @param $context
|
||||
* @return void
|
||||
*/
|
||||
public function testXMLBaseTypes($typehint, $expectedJSONRPCType, $expectedXMLRPCType, $context = '')
|
||||
{
|
||||
$type = new Type($typehint, $context);
|
||||
$this->assertEquals($expectedXMLRPCType, $type->getXMLRPCType());
|
||||
}
|
||||
|
||||
public function provideSubTypes()
|
||||
{
|
||||
return [
|
||||
['string', ['string']],
|
||||
['string[]', ['array', 'string']],
|
||||
['string[][]', ['array', 'array', 'string']],
|
||||
['array[][]', ['array', 'array', 'array']],
|
||||
['Foo[][]', ['array', 'array', 'Foo']],
|
||||
['Foo[][]', ['array', 'array', 'dokuwiki\\test\\Remote\\OpenApiDoc\\Foo'], self::class],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSubTypes
|
||||
* @param $typehint
|
||||
* @param $expected
|
||||
* @param $context
|
||||
* @return void
|
||||
*/
|
||||
public function testSubType($typehint, $expected, $context = '')
|
||||
{
|
||||
$type = new Type($typehint, $context);
|
||||
|
||||
$result = [$type->getJSONRPCType()];
|
||||
while ($type = $type->getSubType()) {
|
||||
$result[] = $type->getJSONRPCType();
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
}
|
|
@ -254,6 +254,10 @@ class ApiCall
|
|||
$this->return['type'] = 'string';
|
||||
}
|
||||
|
||||
if (isset($docInfo['return']['response'])) {
|
||||
$this->return['response'] = $docInfo['return']['response'];
|
||||
}
|
||||
|
||||
if (isset($docInfo['return']['description'])) {
|
||||
$this->return['description'] = $docInfo['return']['description'];
|
||||
}
|
||||
|
@ -328,6 +332,7 @@ class ApiCall
|
|||
[$type, $description] = array_map('trim', sexplode(' ', $return, 2, ''));
|
||||
$result['return'] = [
|
||||
'type' => $this->cleanTypeHint($type),
|
||||
'response' => $type, // uncleaned
|
||||
'description' => $description
|
||||
];
|
||||
unset($tags['return']);
|
||||
|
|
|
@ -7,6 +7,7 @@ use dokuwiki\ChangeLog\MediaChangeLog;
|
|||
use dokuwiki\ChangeLog\PageChangeLog;
|
||||
use dokuwiki\Extension\AuthPlugin;
|
||||
use dokuwiki\Extension\Event;
|
||||
use dokuwiki\Remote\Response\Page;
|
||||
use dokuwiki\Utf8\Sort;
|
||||
|
||||
/**
|
||||
|
@ -211,12 +212,7 @@ class ApiCore
|
|||
if ($perm < AUTH_READ) {
|
||||
continue;
|
||||
}
|
||||
$page = [];
|
||||
$page['id'] = trim($pages[$idx]);
|
||||
$page['perms'] = $perm;
|
||||
$page['size'] = @filesize(wikiFN($pages[$idx]));
|
||||
$page['lastModified'] = $this->api->toDate(@filemtime(wikiFN($pages[$idx])));
|
||||
$list[] = $page;
|
||||
$list[] = new Page(['id' => $pages[$idx], 'perm' => $perm]);
|
||||
}
|
||||
|
||||
return $list;
|
||||
|
@ -229,7 +225,7 @@ class ApiCore
|
|||
* @param array $opts
|
||||
* $opts['depth'] recursion level, 0 for all
|
||||
* $opts['hash'] do md5 sum of content?
|
||||
* @return array[] A list of matching pages with id, rev, mtime, size, (hash)
|
||||
* @return Page[] A list of matching pages with id, rev, mtime, size, (hash)
|
||||
*/
|
||||
public function readNamespace($ns, $opts = [])
|
||||
{
|
||||
|
@ -242,7 +238,11 @@ class ApiCore
|
|||
$data = [];
|
||||
$opts['skipacl'] = 0; // no ACL skipping for XMLRPC
|
||||
search($data, $conf['datadir'], 'search_allpages', $opts, $dir);
|
||||
return $data;
|
||||
|
||||
$result = array_map(fn($item) => new Page($item), $data);
|
||||
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -78,6 +78,38 @@ class OpenAPIGenerator
|
|||
}
|
||||
}
|
||||
|
||||
protected function addComponents()
|
||||
{
|
||||
$schemas = [];
|
||||
|
||||
$files = glob(DOKU_INC . 'inc/Remote/Response/*.php');
|
||||
foreach ($files as $file) {
|
||||
$name = basename($file, '.php');
|
||||
$class = 'dokuwiki\\Remote\\Response\\' . $name;
|
||||
$reflection = new \ReflectionClass($class);
|
||||
if($reflection->isAbstract()) continue;
|
||||
|
||||
$classDoc = new OpenApiDoc\DocBlockClass($reflection);
|
||||
|
||||
|
||||
$schemas[$name] = [
|
||||
'type' => 'object',
|
||||
'summary' => $classDoc->getSummary(),
|
||||
'description' => $classDoc->getDescription(),
|
||||
'properties' => [],
|
||||
];
|
||||
|
||||
foreach ($classDoc->getPropertyDocs() as $property => $doc) {
|
||||
$schemas[$name]['properties'][$property] = [
|
||||
'type' => $this->fixTypes($doc->getTag('type')),
|
||||
'description' => $doc->getSummary(),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected function getMethodDefinition(string $method, ApiCall $call)
|
||||
{
|
||||
$retType = $this->fixTypes($call->getReturn()['type']);
|
||||
|
@ -168,6 +200,9 @@ class OpenAPIGenerator
|
|||
return $schema;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
protected function fixTypes($type)
|
||||
{
|
||||
switch ($type) {
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\Remote\OpenApiDoc;
|
||||
|
||||
|
||||
class ClassResolver
|
||||
{
|
||||
|
||||
/** @var ClassResolver */
|
||||
private static $instance;
|
||||
|
||||
protected $classUses = [];
|
||||
protected $classDocs = [];
|
||||
|
||||
/**
|
||||
* @internal Use ClassResolver::getInstance() instead
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a singleton instance
|
||||
*
|
||||
* Constructor is public for testing purposes
|
||||
* @return ClassResolver
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a class name to a fully qualified class name
|
||||
*
|
||||
* Results are cached in the instance for reuse
|
||||
*
|
||||
* @param string $classalias The class name to resolve
|
||||
* @param string $context The classname in which context in which the class is used
|
||||
* @return string No guarantee that the class exists! No leading backslash!
|
||||
*/
|
||||
public function resolve($classalias, $context)
|
||||
{
|
||||
if ($classalias[0] === '\\') {
|
||||
// Fully qualified class name given
|
||||
return ltrim($classalias, '\\');
|
||||
}
|
||||
$classinfo = $this->getClassUses($context);
|
||||
|
||||
if (isset($classinfo['uses'][$classalias])) {
|
||||
return $classinfo['uses'][$classalias];
|
||||
}
|
||||
|
||||
return $classinfo['ownNS'] . '\\' . $classalias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a class name to a fully qualified class name and return a DocBlockClass for it
|
||||
*
|
||||
* Results are cached in the instance for reuse
|
||||
*
|
||||
* @param string $classalias The class name to resolve
|
||||
* @param string $context The classname in which context in which the class is used
|
||||
* @return DocBlockClass|null
|
||||
*/
|
||||
public function document($classalias, $context)
|
||||
{
|
||||
$class = $this->resolve($classalias, $context);
|
||||
if(!class_exists($class)) return null;
|
||||
|
||||
if(isset($this->classDocs[$class])) {
|
||||
$reflector = new \ReflectionClass($class);
|
||||
$this->classDocs[$class] = new DocBlockClass($reflector);
|
||||
}
|
||||
|
||||
return $this->classDocs[$class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached fetching of all defined class aliases
|
||||
*
|
||||
* @param string $class The class to parse
|
||||
* @return array
|
||||
*/
|
||||
public function getClassUses($class)
|
||||
{
|
||||
if (!isset($this->classUses[$class])) {
|
||||
$reflector = new \ReflectionClass($class);
|
||||
$source = $this->readSource($reflector->getFileName(), $reflector->getStartLine());
|
||||
$this->classUses[$class] = [
|
||||
'ownNS' => $reflector->getNamespaceName(),
|
||||
'uses' => $this->tokenizeSource($source)
|
||||
];
|
||||
}
|
||||
return $this->classUses[$class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the use statements from the given source code
|
||||
*
|
||||
* This is a simplified version of the code by @jasondmoss - we do not support multiple
|
||||
* classed within one file
|
||||
*
|
||||
* @link https://gist.github.com/jasondmoss/6200807
|
||||
* @param string $source
|
||||
* @return array
|
||||
*/
|
||||
private function tokenizeSource($source)
|
||||
{
|
||||
|
||||
$tokens = token_get_all($source);
|
||||
|
||||
$useStatements = [];
|
||||
$record = false;
|
||||
$currentUse = [
|
||||
'class' => '',
|
||||
'as' => ''
|
||||
];
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if (!is_array($token)) {
|
||||
// statement ended
|
||||
if ($record) {
|
||||
$useStatements[] = $currentUse;
|
||||
$record = false;
|
||||
$currentUse = [
|
||||
'class' => '',
|
||||
'as' => ''
|
||||
];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$tokenname = token_name($token[0]);
|
||||
|
||||
if ($token[0] === T_CLASS) {
|
||||
break; // we reached the class itself, no need to parse further
|
||||
}
|
||||
|
||||
if ($token[0] === T_USE) {
|
||||
$record = 'class';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($token[0] === T_AS) {
|
||||
$record = 'as';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($record) {
|
||||
switch ($token[0]) {
|
||||
case T_STRING:
|
||||
case T_NS_SEPARATOR:
|
||||
case T_NAME_QUALIFIED:
|
||||
$currentUse[$record] .= $token[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return a lookup table alias to FQCN
|
||||
$table = [];
|
||||
foreach ($useStatements as $useStatement) {
|
||||
$class = $useStatement['class'];
|
||||
$alias = $useStatement['as'] ?: substr($class, strrpos($class, '\\') + 1);
|
||||
$table[$alias] = $class;
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read file source up to the line where our class is defined.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function readSource($file, $startline)
|
||||
{
|
||||
$file = fopen($file, 'r');
|
||||
$line = 0;
|
||||
$source = '';
|
||||
|
||||
while (!feof($file)) {
|
||||
++$line;
|
||||
|
||||
if ($line >= $startline) {
|
||||
break;
|
||||
}
|
||||
|
||||
$source .= fgets($file);
|
||||
}
|
||||
fclose($file);
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\Remote\OpenApiDoc;
|
||||
|
||||
use Reflector;
|
||||
|
||||
class DocBlock
|
||||
{
|
||||
/** @var Reflector The reflected object */
|
||||
protected $reflector;
|
||||
|
||||
/** @var string The first line of the decription */
|
||||
protected $summary = '';
|
||||
|
||||
/** @var string The description */
|
||||
protected $description = '';
|
||||
|
||||
/** @var string The parsed tags */
|
||||
protected $tags = [];
|
||||
|
||||
/**
|
||||
* Parse the given docblock
|
||||
*
|
||||
* The docblock can be of a method, class or property.
|
||||
*
|
||||
* @param Reflector $reflector
|
||||
*/
|
||||
public function __construct(Reflector $reflector)
|
||||
{
|
||||
$this->reflector = $reflector;
|
||||
$docblock = $reflector->getDocComment();
|
||||
|
||||
// strip asterisks and leading spaces
|
||||
$docblock = trim(preg_replace(
|
||||
['/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'],
|
||||
['', '', '', ''],
|
||||
$docblock
|
||||
));
|
||||
|
||||
// get all tags
|
||||
$tags = [];
|
||||
if (preg_match_all('/^@(\w+)\s+(.*)$/m', $docblock, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $match) {
|
||||
$tags[$match[1]][] = trim($match[2]);
|
||||
}
|
||||
}
|
||||
|
||||
// strip the tags from the docblock
|
||||
$docblock = preg_replace('/^@(\w+)\s+(.*)$/m', '', $docblock);
|
||||
|
||||
// what remains is summary and description
|
||||
[$summary, $description] = sexplode("\n\n", $docblock, 2, '');
|
||||
|
||||
// store everything
|
||||
$this->summary = trim($summary);
|
||||
$this->description = trim($description);
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* The class name of the declaring class
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getContext()
|
||||
{
|
||||
return $this->reflector->getDeclaringClass()->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSummary(): string
|
||||
{
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific tag
|
||||
*
|
||||
* @param string $tag
|
||||
* @return array
|
||||
*/
|
||||
public function getTag($tag)
|
||||
{
|
||||
if (!isset($this->tags[$tag])) return [];
|
||||
return $this->tags[$tag];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\Remote\OpenApiDoc;
|
||||
|
||||
use ReflectionClass;
|
||||
|
||||
class DocBlockClass extends DocBlock
|
||||
{
|
||||
/** @var DocBlockMethod[] */
|
||||
protected $methods = [];
|
||||
|
||||
/** @var DocBlockProperty[] */
|
||||
protected $properties = [];
|
||||
|
||||
/**
|
||||
* Parse the given docblock
|
||||
*
|
||||
* The docblock can be of a method, class or property.
|
||||
*
|
||||
* @param ReflectionClass $reflector
|
||||
*/
|
||||
public function __construct(ReflectionClass $reflector)
|
||||
{
|
||||
parent::__construct($reflector);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function getContext()
|
||||
{
|
||||
return $this->reflector->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public methods of this class
|
||||
*
|
||||
* @return DocBlockMethod[]
|
||||
*/
|
||||
public function getMethodDocs()
|
||||
{
|
||||
if ($this->methods) return $this->methods;
|
||||
|
||||
foreach ($this->reflector->getMethods() as $method) {
|
||||
/** @var \ReflectionMethod $method */
|
||||
if ($method->isConstructor()) continue;
|
||||
if ($method->isDestructor()) continue;
|
||||
if (!$method->isPublic()) continue;
|
||||
$this->methods[$method->getName()] = new DocBlockMethod($method);
|
||||
}
|
||||
|
||||
return $this->methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public properties of this class
|
||||
*
|
||||
* @return DocBlockProperty[]
|
||||
*/
|
||||
public function getPropertyDocs()
|
||||
{
|
||||
if ($this->properties) return $this->properties;
|
||||
|
||||
foreach ($this->reflector->getProperties() as $property) {
|
||||
/** @var \ReflectionProperty $property */
|
||||
if (!$property->isPublic()) continue;
|
||||
$this->properties[$property->getName()] = new DocBlockProperty($property);
|
||||
}
|
||||
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\Remote\OpenApiDoc;
|
||||
|
||||
use ReflectionMethod;
|
||||
|
||||
class DocBlockMethod extends DocBlock
|
||||
{
|
||||
|
||||
/**
|
||||
* Parse the given docblock
|
||||
*
|
||||
* The docblock can be of a method, class or property.
|
||||
*
|
||||
* @param ReflectionMethod $reflector
|
||||
*/
|
||||
public function __construct(ReflectionMethod $reflector)
|
||||
{
|
||||
parent::__construct($reflector);
|
||||
$this->refineParam();
|
||||
$this->refineReturn();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Parse the param tag into its components
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function refineParam()
|
||||
{
|
||||
$result = [];
|
||||
|
||||
// prefill from reflection
|
||||
foreach ($this->reflector->getParameters() as $parameter) {
|
||||
$refType = $parameter->getType();
|
||||
$result[$parameter->getName()] = [
|
||||
'type' => new Type($refType ? $refType->getName() : 'string', $this->getContext()),
|
||||
'optional' => $parameter->isOptional(),
|
||||
'description' => '',
|
||||
];
|
||||
if($parameter->isDefaultValueAvailable()) {
|
||||
$result[$parameter->getName()]['default'] = $parameter->getDefaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
// refine from doc tags
|
||||
foreach ($this->tags['param'] ?? [] as $param) {
|
||||
[$type, $name, $description] = array_map('trim', sexplode(' ', $param, 3, ''));
|
||||
if ($name === '' || $name[0] !== '$') continue;
|
||||
$name = substr($name, 1);
|
||||
if (!isset($result[$name])) continue; // reflection says this param does not exist
|
||||
|
||||
$result[$name]['type'] = new Type($type, $this->getContext());
|
||||
$result[$name]['description'] = $description;
|
||||
}
|
||||
$this->tags['param'] = $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the return tag into its components
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function refineReturn()
|
||||
{
|
||||
|
||||
|
||||
// prefill from reflection
|
||||
$refType = $this->reflector->getReturnType();
|
||||
$result = [
|
||||
'type' => new Type($refType ? $refType->getName() : 'void', $this->getContext()),
|
||||
'description' => '',
|
||||
];
|
||||
|
||||
// refine from doc tag
|
||||
foreach ($this->tags['return'] ?? [] as $return) {
|
||||
[$type, $description] = array_map('trim', sexplode(' ', $return, 2, ''));
|
||||
$result['type'] = new Type($type);
|
||||
$result['description'] = $description;
|
||||
|
||||
}
|
||||
$this->tags['return'] = $result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\Remote\OpenApiDoc;
|
||||
|
||||
class DocBlockProperty extends DocBlock
|
||||
{
|
||||
|
||||
/** @var Type */
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* Parse the given docblock
|
||||
*
|
||||
* The docblock can be of a method, class or property.
|
||||
*
|
||||
* @param \ReflectionProperty $reflector
|
||||
*/
|
||||
public function __construct(\ReflectionProperty $reflector)
|
||||
{
|
||||
parent::__construct($reflector);
|
||||
$this->refineVar();
|
||||
}
|
||||
|
||||
/**
|
||||
* The Type of this property
|
||||
*
|
||||
* @return Type
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the var tag into its components
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function refineVar()
|
||||
{
|
||||
$refType = $this->reflector->getType();
|
||||
$this->type = new Type($refType ? $refType->getName() : 'string', $this->getContext());
|
||||
|
||||
|
||||
if (!isset($this->tags['var'])) return;
|
||||
|
||||
[$type, $description] = array_map('trim', sexplode(' ', $this->tags['var'][0], 2, ''));
|
||||
$this->type = new Type($type, $this->getContext());
|
||||
$this->summary = $description;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\Remote\OpenApiDoc;
|
||||
|
||||
class Type
|
||||
{
|
||||
protected $typehint;
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* @param string $typehint The typehint as read from the docblock
|
||||
* @param string $context A fully qualified class name in which context the typehint is used
|
||||
*/
|
||||
public function __construct($typehint, $context = '')
|
||||
{
|
||||
$this->typehint = $typehint;
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a primitive PHP type
|
||||
*
|
||||
* @param string $typehint
|
||||
* @return string
|
||||
*/
|
||||
protected function toPrimitiveType($typehint)
|
||||
{
|
||||
if (str_ends_with($typehint, '[]')) {
|
||||
return 'array';
|
||||
}
|
||||
|
||||
if (in_array($typehint, ['boolean', 'false', 'true'])) {
|
||||
return 'bool';
|
||||
}
|
||||
|
||||
if (in_array($typehint, ['integer', 'date'])) {
|
||||
return 'int';
|
||||
}
|
||||
|
||||
if ($typehint === 'file') {
|
||||
return 'string';
|
||||
}
|
||||
|
||||
// fully qualified class name
|
||||
if ($typehint[0] === '\\') {
|
||||
return ltrim($typehint, '\\');
|
||||
}
|
||||
|
||||
// relative class name, try to resolve
|
||||
if ($this->context && ctype_upper($typehint[0])) {
|
||||
return ClassResolver::getInstance()->resolve($typehint, $this->context);
|
||||
}
|
||||
|
||||
return $typehint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a primitive type understood by the XMLRPC server
|
||||
*
|
||||
* @param string $typehint
|
||||
* @return string
|
||||
*/
|
||||
public function getJSONRPCType()
|
||||
{
|
||||
return $this->toPrimitiveType($this->typehint);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is an array, return the type of the array elements
|
||||
*
|
||||
* @return Type|null null if this is not a typed array
|
||||
*/
|
||||
public function getSubType()
|
||||
{
|
||||
$type = $this->typehint;
|
||||
if (!str_ends_with($type, '[]')) {
|
||||
return null;
|
||||
}
|
||||
$type = substr($type, 0, -2);
|
||||
return new Type($type, $this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a type understood by the XMLRPC server
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getXMLRPCType()
|
||||
{
|
||||
$type = $this->typehint;
|
||||
|
||||
// keep custom types
|
||||
if (in_array($type, ['date', 'file', 'struct'])) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
$type = $this->toPrimitiveType($this->typehint);
|
||||
|
||||
// primitive types
|
||||
if (in_array($type, ['int', 'string', 'double', 'bool', 'array'])) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
// everything else is an object
|
||||
return 'object'; //should this return 'struct'?
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\Remote\Response;
|
||||
|
||||
/**
|
||||
* These are simple data objects that hold the response data API calls
|
||||
*
|
||||
* They are transmitted as associative arrays automatically created by
|
||||
* converting the object to an array
|
||||
*/
|
||||
abstract class ApiResponse
|
||||
{
|
||||
/**
|
||||
* Initialize the response object with the given data
|
||||
*
|
||||
* Each response object has different properties and might get passed different data from
|
||||
* various internal methods. The constructor should handle all of that and also fill up
|
||||
* missing properties when needed.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
abstract public function __construct($data);
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace dokuwiki\Remote\Response;
|
||||
|
||||
/**
|
||||
* Represents a single page revision in the wiki.
|
||||
*/
|
||||
class Page extends ApiResponse
|
||||
{
|
||||
/** @var string The page ID */
|
||||
public $id;
|
||||
/** @var int The page revision aka last modified timestamp */
|
||||
public $revision;
|
||||
/** @var int The page size in bytes */
|
||||
public $size;
|
||||
/** @var string The page title */
|
||||
public $title;
|
||||
/** @var int The current user's permissions for this page */
|
||||
public $perms;
|
||||
/** @var string MD5 sum over the page's content (only if requested) */
|
||||
public $hash;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->id = cleanID($data['id'] ?? '');
|
||||
if ($this->id === '') {
|
||||
throw new \InvalidArgumentException('Missing id');
|
||||
}
|
||||
|
||||
$this->revision = (int)($data['rev'] ?? $data['lastModified'] ?? @filemtime(wikiFN($this->id)));
|
||||
$this->size = (int)($data['size'] ?? @filesize(wikiFN($this->id)));
|
||||
$this->title = $data['title'] ?? $this->retrieveTitle();
|
||||
$this->perms = $data['perm'] ?? auth_quickaclcheck($this->id);
|
||||
$this->hash = $data['hash'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title for the page
|
||||
*
|
||||
* Honors $conf['useheading']
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function retrieveTitle()
|
||||
{
|
||||
global $conf;
|
||||
|
||||
if ($conf['useheading']) {
|
||||
$title = p_get_first_heading($this->id);
|
||||
if ($title) {
|
||||
return $title;
|
||||
}
|
||||
}
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue