]> BookStack Code Mirror - bookstack/commitdiff
Started work on generating API docs
authorDan Brown <redacted>
Sun, 12 Jan 2020 16:25:14 +0000 (16:25 +0000)
committerDan Brown <redacted>
Sun, 12 Jan 2020 16:25:14 +0000 (16:25 +0000)
app/Api/ApiDocsGenerator.php [new file with mode: 0644]
app/Http/Controllers/Api/ApiDocsController.php [new file with mode: 0644]
app/Http/Controllers/Api/BooksApiController.php
routes/api.php

diff --git a/app/Api/ApiDocsGenerator.php b/app/Api/ApiDocsGenerator.php
new file mode 100644 (file)
index 0000000..b634066
--- /dev/null
@@ -0,0 +1,122 @@
+<?php namespace BookStack\Api;
+
+use BookStack\Http\Controllers\Api\ApiController;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Route;
+use ReflectionClass;
+use ReflectionException;
+use ReflectionMethod;
+
+class ApiDocsGenerator
+{
+
+    protected $reflectionClasses = [];
+    protected $controllerClasses = [];
+
+    /**
+     * Generate API documentation.
+     */
+    public function generate(): Collection
+    {
+        $apiRoutes = $this->getFlatApiRoutes();
+        $apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
+        $apiRoutes = $this->loadDetailsFromFiles($apiRoutes);
+        $apiRoutes = $apiRoutes->groupBy('base_model');
+        return $apiRoutes;
+    }
+
+    /**
+     * Load any API details stored in static files.
+     */
+    protected function loadDetailsFromFiles(Collection $routes): Collection
+    {
+        return $routes->map(function (array $route) {
+            $exampleResponseFile = base_path('dev/api/responses/' . $route['name'] . '.json');
+            $exampleResponse = file_exists($exampleResponseFile) ? file_get_contents($exampleResponseFile) : null;
+            $route['example_response'] = $exampleResponse;
+            return $route;
+        });
+    }
+
+    /**
+     * Load any details we can fetch from the controller and its methods.
+     */
+    protected function loadDetailsFromControllers(Collection $routes): Collection
+    {
+        return $routes->map(function (array $route) {
+            $method = $this->getReflectionMethod($route['controller'], $route['controller_method']);
+            $comment = $method->getDocComment();
+            $route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null;
+            $route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']);
+            return $route;
+        });
+    }
+
+    /**
+     * Load body params and their rules by inspecting the given class and method name.
+     * @throws \Illuminate\Contracts\Container\BindingResolutionException
+     */
+    protected function getBodyParamsFromClass(string $className, string $methodName): ?array
+    {
+        /** @var ApiController $class */
+        $class = $this->controllerClasses[$className] ?? null;
+        if ($class === null) {
+            $class = app()->make($className);
+            $this->controllerClasses[$className] = $class;
+        }
+
+        $rules = $class->getValdationRules()[$methodName] ?? [];
+        foreach ($rules as $param => $ruleString) {
+            $rules[$param] = explode('|', $ruleString);
+        }
+        return count($rules) > 0 ? $rules : null;
+    }
+
+    /**
+     * Parse out the description text from a class method comment.
+     */
+    protected function parseDescriptionFromMethodComment(string $comment)
+    {
+        $matches = [];
+        preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches);
+        return implode(' ', $matches[1] ?? []);
+    }
+
+    /**
+     * Get a reflection method from the given class name and method name.
+     * @throws ReflectionException
+     */
+    protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod
+    {
+        $class = $this->reflectionClasses[$className] ?? null;
+        if ($class === null) {
+            $class = new ReflectionClass($className);
+            $this->reflectionClasses[$className] = $class;
+        }
+
+        return $class->getMethod($methodName);
+    }
+
+    /**
+     * Get the system API routes, formatted into a flat collection.
+     */
+    protected function getFlatApiRoutes(): Collection
+    {
+        return collect(Route::getRoutes()->getRoutes())->filter(function ($route) {
+            return strpos($route->uri, 'api/') === 0;
+        })->map(function ($route) {
+            [$controller, $controllerMethod] = explode('@', $route->action['uses']);
+            $baseModelName = explode('/', $route->uri)[1];
+            $shortName = $baseModelName . '-' . $controllerMethod;
+            return [
+                'name' => $shortName,
+                'uri' => $route->uri,
+                'method' => $route->methods[0],
+                'controller' => $controller,
+                'controller_method' => $controllerMethod,
+                'base_model' => $baseModelName,
+            ];
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/app/Http/Controllers/Api/ApiDocsController.php b/app/Http/Controllers/Api/ApiDocsController.php
new file mode 100644 (file)
index 0000000..bfb0c18
--- /dev/null
@@ -0,0 +1,47 @@
+<?php namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Api\ApiDocsGenerator;
+use Cache;
+use Illuminate\Support\Collection;
+
+class ApiDocsController extends ApiController
+{
+
+    /**
+     * Load the docs page for the API.
+     */
+    public function display()
+    {
+        $docs = $this->getDocs();
+        dd($docs);
+        // TODO - Build view for API docs
+        return view('');
+    }
+
+    /**
+     * Show a JSON view of the API docs data.
+     */
+    public function json() {
+        $docs = $this->getDocs();
+        return response()->json($docs);
+    }
+
+    /**
+     * Get the base docs data.
+     * Checks and uses the system cache for quick re-fetching.
+     */
+    protected function getDocs(): Collection
+    {
+        $appVersion = trim(file_get_contents(base_path('version')));
+        $cacheKey = 'api-docs::' . $appVersion;
+        if (Cache::has($cacheKey) && config('app.env') === 'production') {
+            $docs = Cache::get($cacheKey);
+        } else {
+            $docs = (new ApiDocsGenerator())->generate();
+            Cache::put($cacheKey, $docs, 60*24);
+        }
+
+        return $docs;
+    }
+
+}
index e7a0217dcdeaa2aa507a908ecc801cf27b1a6819..8c62b7d7d2927bd28d4bd02becd78f5c5baeeb0e 100644 (file)
@@ -31,7 +31,6 @@ class BooksApiController extends ApiController
 
     /**
      * Get a listing of books visible to the user.
-     * @api listing
      */
     public function index()
     {
index 3348d8907b909a0901269afe5790b6d8edb72838..12b327798988753063db47c911d0108c1be16391 100644 (file)
@@ -6,6 +6,9 @@
  * Controllers are all within app/Http/Controllers/Api
  */
 
+Route::get('docs', 'ApiDocsController@display');
+Route::get('docs.json', 'ApiDocsController@json');
+
 Route::get('books', 'BooksApiController@index');
 Route::post('books', 'BooksApiController@create');
 Route::get('books/{id}', 'BooksApiController@read');