diff --git a/app/Api/ApiDocsGenerator.php b/app/Api/ApiDocsGenerator.php new file mode 100644 index 000000000..b63406696 --- /dev/null +++ b/app/Api/ApiDocsGenerator.php @@ -0,0 +1,122 @@ +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 index 000000000..bfb0c1834 --- /dev/null +++ b/app/Http/Controllers/Api/ApiDocsController.php @@ -0,0 +1,47 @@ +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; + } + +} diff --git a/app/Http/Controllers/Api/BooksApiController.php b/app/Http/Controllers/Api/BooksApiController.php index e7a0217dc..8c62b7d7d 100644 --- a/app/Http/Controllers/Api/BooksApiController.php +++ b/app/Http/Controllers/Api/BooksApiController.php @@ -31,7 +31,6 @@ class BooksApiController extends ApiController /** * Get a listing of books visible to the user. - * @api listing */ public function index() { diff --git a/routes/api.php b/routes/api.php index 3348d8907..12b327798 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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');