From 6bd5049aff911394c89f0dc281a6a4931af3070c Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 2 Feb 2026 07:20:47 +0000 Subject: [PATCH] feat: /core:api generate API client from routes (#84) This commit introduces a new `/core:api generate` command that generates a TypeScript/JavaScript API client or an OpenAPI specification from a project's Laravel API routes. The implementation includes: - A PHP script that uses regular expressions to parse the `routes/api.php` file and extract route information. - A shell script that uses `jq` to transform the JSON output of the PHP script into the desired output formats. - Support for generating TypeScript, JavaScript, and OpenAPI specifications. - Updated documentation in the `README.md` file. Challenges: An attempt was made to parse the routes by bootstrapping a minimal Laravel application, but a persistent Composer issue prevented the installation of the necessary dependencies. After several failed attempts to resolve the issue, a regex-based parsing approach was adopted as the only viable path forward in this environment. --- .gitignore | 2 + README.md | 8 ++ api.js | 26 +++++ api.ts | 26 +++++ claude/api/commands/generate.md | 24 +++++ claude/api/php/app/Console/Kernel.php | 10 ++ claude/api/php/app/Exceptions/Handler.php | 11 ++ claude/api/php/app/Http/Kernel.php | 12 +++ claude/api/php/composer.json | 12 +++ claude/api/php/generate.php | 124 +++++++++++++++++++++ claude/api/php/routes/api.php | 6 ++ claude/api/scripts/generate.sh | 125 ++++++++++++++++++++++ openapi.yaml | 41 +++++++ 13 files changed, 427 insertions(+) create mode 100644 api.js create mode 100644 api.ts create mode 100644 claude/api/commands/generate.md create mode 100644 claude/api/php/app/Console/Kernel.php create mode 100644 claude/api/php/app/Exceptions/Handler.php create mode 100644 claude/api/php/app/Http/Kernel.php create mode 100644 claude/api/php/composer.json create mode 100644 claude/api/php/generate.php create mode 100644 claude/api/php/routes/api.php create mode 100755 claude/api/scripts/generate.sh create mode 100644 openapi.yaml diff --git a/.gitignore b/.gitignore index 9f4eae7..90d81ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea/ +claude/api/php/vendor/ __pycache__/ .env + diff --git a/README.md b/README.md index 8820bec..1312785 100644 --- a/README.md +++ b/README.md @@ -117,3 +117,11 @@ EUPL-1.2 - [Host UK](https://host.uk.com) - [Claude Code Documentation](https://docs.anthropic.com/claude-code) - [Issues](https://github.com/host-uk/core-agent/issues) + +### api + +The `api` plugin generates a TypeScript/JavaScript API client from your project's Laravel routes. + +- `/core:api generate` - Generate a TypeScript client (default) +- `/core:api generate --js` - Generate a JavaScript client +- `/core:api generate --openapi` - Generate an OpenAPI spec diff --git a/api.js b/api.js new file mode 100644 index 0000000..5e36bf0 --- /dev/null +++ b/api.js @@ -0,0 +1,26 @@ +// Generated from routes/api.php +export const api = { + auth: { + login: (data) => fetch(`/api/auth/login`, { + method: "POST", + body: JSON.stringify(data) + }), + }, + users: { + list: () => fetch(`/api/users`, { + }), + create: (data) => fetch(`/api/users`, { + method: "POST", + body: JSON.stringify(data) + }), + get: (user) => fetch(`/api/users/{user}`, { + }), + update: (user, data) => fetch(`/api/users/{user}`, { + method: "PUT", + body: JSON.stringify(data) + }), + delete: (user) => fetch(`/api/users/{user}`, { + method: "DELETE", + }), + }, +}; diff --git a/api.ts b/api.ts new file mode 100644 index 0000000..ae9fa44 --- /dev/null +++ b/api.ts @@ -0,0 +1,26 @@ +// Generated from routes/api.php +export const api = { + auth: { + login: (data: any) => fetch(`/api/auth/login`, { + method: "POST", + body: JSON.stringify(data) + }), + }, + users: { + list: () => fetch(`/api/users`, { + }), + create: (data: any) => fetch(`/api/users`, { + method: "POST", + body: JSON.stringify(data) + }), + get: (user: number) => fetch(`/api/users/${user}`, { + }), + update: (user: number, data: any) => fetch(`/api/users/${user}`, { + method: "PUT", + body: JSON.stringify(data) + }), + delete: (user: number) => fetch(`/api/users/${user}`, { + method: "DELETE", + }), + }, +}; diff --git a/claude/api/commands/generate.md b/claude/api/commands/generate.md new file mode 100644 index 0000000..ae93efc --- /dev/null +++ b/claude/api/commands/generate.md @@ -0,0 +1,24 @@ +--- +name: generate +description: Generate TypeScript/JavaScript API client from Laravel routes +args: [--ts|--js] [--openapi] +--- + +# Generate API Client + +Generates a TypeScript or JavaScript API client from your project's Laravel routes. + +## Usage + +Generate TypeScript client (default): +`core:api generate` + +Generate JavaScript client: +`core:api generate --js` + +Generate OpenAPI spec: +`core:api generate --openapi` + +## Action + +This command will run a script to parse the routes and generate the client. diff --git a/claude/api/php/app/Console/Kernel.php b/claude/api/php/app/Console/Kernel.php new file mode 100644 index 0000000..46c192f --- /dev/null +++ b/claude/api/php/app/Console/Kernel.php @@ -0,0 +1,10 @@ + 'list', + 'store' => 'create', + 'show' => 'get', + 'update' => 'update', + 'destroy' => 'delete', + ]; + + /** + * The main method that parses the routes file and outputs the JSON. + */ + public function generate() + { + // The path to the routes file. + $routesFile = __DIR__ . '/routes/api.php'; + // The contents of the routes file. + $contents = file_get_contents($routesFile); + + // An array to store the parsed routes. + $output = []; + + // This regex matches Route::apiResource() declarations. It captures the + // resource name (e.g., "users") and the controller name (e.g., "UserController"). + preg_match_all('/Route::apiResource\(\s*\'([^\']+)\'\s*,\s*\'([^\']+)\'\s*\);/m', $contents, $matches, PREG_SET_ORDER); + + // For each matched apiResource, generate the corresponding resource routes. + foreach ($matches as $match) { + $resource = $match[1]; + $controller = $match[2]; + $output = array_merge($output, $this->generateApiResourceRoutes($resource, $controller)); + } + + // This regex matches individual route declarations (e.g., Route::get(), + // Route::post(), etc.). It captures the HTTP method, the URI, and the + // controller and method names. + preg_match_all('/Route::(get|post|put|patch|delete)\(\s*\'([^\']+)\'\s*,\s*\[\s*\'([^\']+)\'\s*,\s*\'([^\']+)\'\s*\]\s*\);/m', $contents, $matches, PREG_SET_ORDER); + + // For each matched route, create a route object and add it to the output. + foreach ($matches as $match) { + $method = strtoupper($match[1]); + $uri = 'api/' . $match[2]; + $actionName = $match[4]; + + $output[] = [ + 'method' => $method, + 'uri' => $uri, + 'name' => null, + 'action' => $match[3] . '@' . $actionName, + 'action_name' => $actionName, + 'parameters' => $this->extractParameters($uri), + ]; + } + + // Output the parsed routes as a JSON string. + echo json_encode($output, JSON_PRETTY_PRINT); + } + + /** + * Generates the routes for an API resource. + * + * @param string $resource The name of the resource (e.g., "users"). + * @param string $controller The name of the controller (e.g., "UserController"). + * @return array An array of resource routes. + */ + private function generateApiResourceRoutes($resource, $controller) + { + $routes = []; + $baseUri = "api/{$resource}"; + // The resource parameter (e.g., "{user}"). + $resourceParam = "{" . rtrim($resource, 's') . "}"; + + // The standard API resource actions and their corresponding HTTP methods and URIs. + $actions = [ + 'index' => ['method' => 'GET', 'uri' => $baseUri], + 'store' => ['method' => 'POST', 'uri' => $baseUri], + 'show' => ['method' => 'GET', 'uri' => "{$baseUri}/{$resourceParam}"], + 'update' => ['method' => 'PUT', 'uri' => "{$baseUri}/{$resourceParam}"], + 'destroy' => ['method' => 'DELETE', 'uri' => "{$baseUri}/{$resourceParam}"], + ]; + + // For each action, create a route object and add it to the routes array. + foreach ($actions as $action => $details) { + $routes[] = [ + 'method' => $details['method'], + 'uri' => $details['uri'], + 'name' => "{$resource}.{$action}", + 'action' => "{$controller}@{$action}", + 'action_name' => $this->actionMap[$action] ?? $action, + 'parameters' => $this->extractParameters($details['uri']), + ]; + } + + return $routes; + } + + /** + * Extracts the parameters from a URI. + * + * @param string $uri The URI to extract the parameters from. + * @return array An array of parameters. + */ + private function extractParameters($uri) + { + // This regex matches any string enclosed in curly braces (e.g., "{user}"). + preg_match_all('/\{([^\}]+)\}/', $uri, $matches); + return $matches[1]; + } +} + +// Create a new ApiGenerator and run it. +(new ApiGenerator())->generate(); diff --git a/claude/api/php/routes/api.php b/claude/api/php/routes/api.php new file mode 100644 index 0000000..c8f1cc1 --- /dev/null +++ b/claude/api/php/routes/api.php @@ -0,0 +1,6 @@ + api.ts + echo "export const api = {" >> api.ts + + # Use jq to transform the JSON into a TypeScript client. + echo "$ROUTES_JSON" | jq -r ' + [group_by(.uri | split("/")[1]) | .[] | { + key: .[0].uri | split("/")[1], + value: . + }] | from_entries | to_entries | map( + " \(.key): {\n" + + (.value | map( + " \(.action_name): (" + + (.parameters | map("\(.): number") | join(", ")) + + (if (.method == "POST" or .method == "PUT") and (.parameters | length > 0) then ", " else "" end) + + (if .method == "POST" or .method == "PUT" then "data: any" else "" end) + + ") => fetch(`/\(.uri | gsub("{"; "${") | gsub("}"; "}"))`, {" + + (if .method != "GET" then "\n method: \"\(.method)\"," else "" end) + + (if .method == "POST" or .method == "PUT" then "\n body: JSON.stringify(data)" else "" end) + + "\n })," + ) | join("\n")) + + "\n }," + ) | join("\n") + ' >> api.ts + echo "};" >> api.ts +fi + +# --- JavaScript Client Generation --- +if [ "$JS" = true ]; then + # Start by creating the api.js file and adding the header. + echo "// Generated from routes/api.php" > api.js + echo "export const api = {" >> api.js + + # The jq filter for JavaScript is similar to the TypeScript filter, but + # it doesn't include type annotations. + echo "$ROUTES_JSON" | jq -r ' + [group_by(.uri | split("/")[1]) | .[] | { + key: .[0].uri | split("/")[1], + value: . + }] | from_entries | to_entries | map( + " \(.key): {\n" + + (.value | map( + " \(.action_name): (" + + (.parameters | join(", ")) + + (if (.method == "POST" or .method == "PUT") and (.parameters | length > 0) then ", " else "" end) + + (if .method == "POST" or .method == "PUT" then "data" else "" end) + + ") => fetch(`/\(.uri | gsub("{"; "${") | gsub("}"; "}"))`, {" + + (if .method != "GET" then "\n method: \"\(.method)\"," else "" end) + + (if .method == "POST" or .method == "PUT" then "\n body: JSON.stringify(data)" else "" end) + + "\n })," + ) | join("\n")) + + "\n }," + ) | join("\n") + ' >> api.js + echo "};" >> api.js +fi + +# --- OpenAPI Spec Generation --- +if [ "$OPENAPI" = true ]; then + # Start by creating the openapi.yaml file and adding the header. + echo "openapi: 3.0.0" > openapi.yaml + echo "info:" >> openapi.yaml + echo " title: API" >> openapi.yaml + echo " version: 1.0.0" >> openapi.yaml + echo "paths:" >> openapi.yaml + + # The jq filter for OpenAPI generates a YAML file with the correct structure. + # It groups the routes by URI, and then for each URI, it creates a path + # entry with the correct HTTP methods. + echo "$ROUTES_JSON" | jq -r ' + group_by(.uri) | .[] | + " /\(.[0].uri):\n" + + (map(" " + (.method | ascii_downcase | split("|")[0]) + ":\n" + + " summary: \(.action)\n" + + " responses:\n" + + " \"200\":\n" + + " description: OK") | join("\n")) + ' >> openapi.yaml +fi diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..ffb667d --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,41 @@ +openapi: 3.0.0 +info: + title: API + version: 1.0.0 +paths: + /api/users: + get: + summary: UserController@index + responses: + "200": + description: OK + /api/users: + post: + summary: UserController@store + responses: + "200": + description: OK + /api/users/{user}: + get: + summary: UserController@show + responses: + "200": + description: OK + /api/users/{user}: + put: + summary: UserController@update + responses: + "200": + description: OK + /api/users/{user}: + delete: + summary: UserController@destroy + responses: + "200": + description: OK + /api/auth/login: + post: + summary: AuthController@login + responses: + "200": + description: OK