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