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