xref: /dokuwiki/inc/Remote/OpenApiDoc/OpenAPIGenerator.php (revision e4e3d43949dfb6b53840595de532df30a6fdba7b)
1<?php
2
3namespace dokuwiki\Remote\OpenApiDoc;
4
5use dokuwiki\Remote\Api;
6use dokuwiki\Remote\ApiCall;
7use dokuwiki\Remote\ApiCore;
8use dokuwiki\Utf8\PhpString;
9use ReflectionClass;
10use ReflectionException;
11use stdClass;
12
13/**
14 * Generates the OpenAPI documentation for the DokuWiki API
15 */
16class OpenAPIGenerator
17{
18    /** @var Api */
19    protected $api;
20
21    /** @var array Holds the documentation tree while building */
22    protected $documentation = [];
23
24    /**
25     * OpenAPIGenerator constructor.
26     */
27    public function __construct()
28    {
29        $this->api = new Api();
30    }
31
32    /**
33     * Generate the OpenAPI documentation
34     *
35     * @return string JSON encoded OpenAPI specification
36     */
37    public function generate()
38    {
39        $this->documentation = [];
40        $this->documentation['openapi'] = '3.1.0';
41        $this->documentation['info'] = [
42            'title' => 'DokuWiki API',
43            'description' => 'The DokuWiki API OpenAPI specification',
44            'version' => ((string)ApiCore::API_VERSION),
45        ];
46
47        $this->addServers();
48        $this->addSecurity();
49        $this->addMethods();
50
51        return json_encode($this->documentation, JSON_PRETTY_PRINT);
52    }
53
54    /**
55     * Add the current DokuWiki instance as a server
56     *
57     * @return void
58     */
59    protected function addServers()
60    {
61        $this->documentation['servers'] = [
62            [
63                'url' => DOKU_URL . 'lib/exe/jsonrpc.php',
64            ],
65        ];
66    }
67
68    /**
69     * Define the default security schemes
70     *
71     * @return void
72     */
73    protected function addSecurity()
74    {
75        $this->documentation['components']['securitySchemes'] = [
76            'basicAuth' => [
77                'type' => 'http',
78                'scheme' => 'basic',
79            ],
80            'jwt' => [
81                'type' => 'http',
82                'scheme' => 'bearer',
83                'bearerFormat' => 'JWT',
84            ]
85        ];
86        $this->documentation['security'] = [
87            [
88                'basicAuth' => [],
89            ],
90            [
91                'jwt' => [],
92            ],
93        ];
94    }
95
96    /**
97     * Add all methods available in the API to the documentation
98     *
99     * @return void
100     */
101    protected function addMethods()
102    {
103        $methods = $this->api->getMethods();
104
105        $this->documentation['paths'] = [];
106        foreach ($methods as $method => $call) {
107            $this->documentation['paths']['/' . $method] = [
108                'post' => $this->getMethodDefinition($method, $call),
109            ];
110        }
111    }
112
113    /**
114     * Create the schema definition for a single API method
115     *
116     * @param string $method API method name
117     * @param ApiCall $call The call definition
118     * @return array
119     */
120    protected function getMethodDefinition(string $method, ApiCall $call)
121    {
122        $description = $call->getDescription();
123        $links = $call->getDocs()->getTag('link');
124        if ($links) {
125            $description .= "\n\n**See also:**";
126            foreach ($links as $link) {
127                $description .= "\n\n* " . $this->generateLink($link);
128            }
129        }
130
131        $retType = $call->getReturn()['type'];
132        $result = array_merge(
133            [
134                'description' => $call->getReturn()['description'],
135                'examples' => [$this->generateExample('result', $retType->getOpenApiType())],
136            ],
137            $this->typeToSchema($retType)
138        );
139
140        $definition = [
141            'operationId' => $method,
142            'summary' => $call->getSummary(),
143            'description' => $description,
144            'tags' => [PhpString::ucwords($call->getCategory())],
145            'requestBody' => [
146                'required' => true,
147                'content' => [
148                    'application/json' => $this->getMethodArguments($call->getArgs()),
149                ]
150            ],
151            'responses' => [
152                200 => [
153                    'description' => 'Result',
154                    'content' => [
155                        'application/json' => [
156                            'schema' => [
157                                'type' => 'object',
158                                'properties' => [
159                                    'result' => $result,
160                                    'error' => [
161                                        'type' => 'object',
162                                        'description' => 'Error object in case of an error',
163                                        'properties' => [
164                                            'code' => [
165                                                'type' => 'integer',
166                                                'description' => 'The error code',
167                                                'examples' => [0],
168                                            ],
169                                            'message' => [
170                                                'type' => 'string',
171                                                'description' => 'The error message',
172                                                'examples' => ['Success'],
173                                            ],
174                                        ],
175                                    ],
176                                ],
177                            ],
178                        ],
179                    ],
180                ],
181            ]
182        ];
183
184        if ($call->isPublic()) {
185            $definition['security'] = [
186                new stdClass(),
187            ];
188            $definition['description'] = 'This method is public and does not require authentication. ' .
189                "\n\n" . $definition['description'];
190        }
191
192        if ($call->getDocs()->getTag('deprecated')) {
193            $definition['deprecated'] = true;
194            $definition['description'] = '**This method is deprecated.** ' .
195                $call->getDocs()->getTag('deprecated')[0] .
196                "\n\n" . $definition['description'];
197        }
198
199        return $definition;
200    }
201
202    /**
203     * Create the schema definition for the arguments of a single API method
204     *
205     * @param array $args The arguments of the method as returned by ApiCall::getArgs()
206     * @return array
207     */
208    protected function getMethodArguments($args)
209    {
210        if (!$args) {
211            // even if no arguments are needed, we need to define a body
212            // this is to ensure the openapi spec knows that a application/json header is needed
213            return ['schema' => ['type' => 'null']];
214        }
215
216        $props = [];
217        $reqs = [];
218        $schema = [
219            'schema' => [
220                'type' => 'object',
221                'required' => &$reqs,
222                'properties' => &$props
223            ]
224        ];
225
226        foreach ($args as $name => $info) {
227            $example = $this->generateExample($name, $info['type']->getOpenApiType());
228
229            $description = $info['description'];
230            if ($info['optional'] && isset($info['default'])) {
231                $description .= ' [_default: `' . json_encode($info['default']) . '`_]';
232            }
233
234            $props[$name] = array_merge(
235                [
236                    'description' => $description,
237                    'examples' => [$example],
238                ],
239                $this->typeToSchema($info['type'])
240            );
241            if (!$info['optional']) $reqs[] = $name;
242        }
243
244
245        return $schema;
246    }
247
248    /**
249     * Generate an example value for the given parameter
250     *
251     * @param string $name The parameter's name
252     * @param string $type The parameter's type
253     * @return mixed
254     */
255    protected function generateExample($name, $type)
256    {
257        switch ($type) {
258            case 'integer':
259                if ($name === 'rev') return 0;
260                if ($name === 'revision') return 0;
261                if ($name === 'timestamp') return time() - 60 * 24 * 30 * 2;
262                return 42;
263            case 'boolean':
264                return true;
265            case 'string':
266                if ($name === 'page') return 'playground:playground';
267                if ($name === 'media') return 'wiki:dokuwiki-128.png';
268                return 'some-' . $name;
269            case 'array':
270                return ['some-' . $name, 'other-' . $name];
271            default:
272                return new stdClass();
273        }
274    }
275
276    /**
277     * Generates a markdown link from a dokuwiki.org URL
278     *
279     * @param $url
280     * @return mixed|string
281     */
282    protected function generateLink($url)
283    {
284        if (preg_match('/^https?:\/\/(www\.)?dokuwiki\.org\/(.+)$/', $url, $match)) {
285            $name = $match[2];
286
287            $name = str_replace(['_', '#', ':'], [' ', ' ', ' '], $name);
288            $name = PhpString::ucwords($name);
289
290            return "[$name]($url)";
291        } else {
292            return $url;
293        }
294    }
295
296
297    /**
298     * Generate the OpenAPI schema for the given type
299     *
300     * @param Type $type
301     * @return array
302     * @todo add example generation here
303     */
304    public function typeToSchema(Type $type)
305    {
306        $schema = [
307            'type' => $type->getOpenApiType(),
308        ];
309
310        // if a sub type is known, define the items
311        if ($schema['type'] === 'array' && $type->getSubType()) {
312            $schema['items'] = $this->typeToSchema($type->getSubType());
313        }
314
315        // if this is an object, define the properties
316        if ($schema['type'] === 'object') {
317            try {
318                $baseType = $type->getBaseType();
319                $doc = new DocBlockClass(new ReflectionClass($baseType));
320                $schema['properties'] = [];
321                foreach ($doc->getPropertyDocs() as $property => $propertyDoc) {
322                    $schema['properties'][$property] = array_merge(
323                        [
324                            'description' => $propertyDoc->getSummary(),
325                        ],
326                        $this->typeToSchema($propertyDoc->getType())
327                    );
328                }
329            } catch (ReflectionException $e) {
330                // The class is not available, so we cannot generate a schema
331            }
332        }
333
334        return $schema;
335    }
336
337}
338