feat(code): add /code:api for API client generation (#108)
Add API client generator command that: - Parses Laravel routes file (routes/api.php) - Generates TypeScript client with DTOs (--ts, default) - Generates JavaScript client (--js) - Generates OpenAPI 3.0 specification (--openapi) - Supports apiResource routes Migrated from core-claude PR #52. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5f136dea2a
commit
e3259257ac
2 changed files with 238 additions and 0 deletions
27
claude/code/commands/api.md
Normal file
27
claude/code/commands/api.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: api
|
||||
description: Generate TypeScript/JavaScript API client from Laravel routes
|
||||
args: generate [--ts|--js|--openapi]
|
||||
---
|
||||
|
||||
# API Client Generator
|
||||
|
||||
Generate a TypeScript/JavaScript API client or an OpenAPI specification from your Laravel routes.
|
||||
|
||||
## Usage
|
||||
|
||||
Generate a TypeScript client (default):
|
||||
`/code:api generate`
|
||||
`/code:api generate --ts`
|
||||
|
||||
Generate a JavaScript client:
|
||||
`/code:api generate --js`
|
||||
|
||||
Generate an OpenAPI specification:
|
||||
`/code:api generate --openapi`
|
||||
|
||||
## Action
|
||||
|
||||
```bash
|
||||
"${CLAUDE_PLUGIN_ROOT}/scripts/api-generate.sh" "$@"
|
||||
```
|
||||
211
claude/code/scripts/api-generate.sh
Executable file
211
claude/code/scripts/api-generate.sh
Executable file
|
|
@ -0,0 +1,211 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Default values
|
||||
output_format="ts"
|
||||
routes_file="routes/api.php"
|
||||
output_file="api_client" # Default output file name without extension
|
||||
|
||||
# Parse command-line arguments
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
generate) ;; # Skip the generate subcommand
|
||||
--ts) output_format="ts";;
|
||||
--js) output_format="js";;
|
||||
--openapi) output_format="openapi";;
|
||||
*) routes_file="$1";;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Set the output file extension based on format
|
||||
if [[ "$output_format" == "openapi" ]]; then
|
||||
output_file="openapi.json"
|
||||
else
|
||||
output_file="api_client.${output_format}"
|
||||
fi
|
||||
|
||||
# Function to parse the routes file
|
||||
parse_routes() {
|
||||
if [ ! -f "$1" ]; then
|
||||
echo "Error: Routes file not found at $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
awk -F"'" '
|
||||
/Route::apiResource/ {
|
||||
resource = $2;
|
||||
resource_singular = resource;
|
||||
sub(/s$/, "", resource_singular);
|
||||
print "GET " resource " list";
|
||||
print "POST " resource " create";
|
||||
print "GET " resource "/{" resource_singular "} get";
|
||||
print "PUT " resource "/{" resource_singular "} update";
|
||||
print "DELETE " resource "/{" resource_singular "} delete";
|
||||
}
|
||||
/Route::(get|post|put|delete|patch)/ {
|
||||
line = $0;
|
||||
match(line, /Route::([a-z]+)/, m);
|
||||
method = toupper(m[1]);
|
||||
uri = $2;
|
||||
action = $6;
|
||||
print method " " uri " " action;
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
# Function to generate the API client
|
||||
generate_client() {
|
||||
local format=$1
|
||||
local outfile=$2
|
||||
local client_object="export const api = {\n"
|
||||
local dto_definitions=""
|
||||
declare -A dtos
|
||||
|
||||
declare -A groups
|
||||
|
||||
# First pass: Collect all routes and DTOs
|
||||
while read -r method uri action; do
|
||||
group=$(echo "$uri" | cut -d'/' -f1)
|
||||
if [[ -z "${groups[$group]}" ]]; then
|
||||
groups[$group]=""
|
||||
fi
|
||||
groups[$group]+="$method $uri $action\n"
|
||||
|
||||
if [[ "$method" == "POST" || "$method" == "PUT" || "$method" == "PATCH" ]]; then
|
||||
local resource_name_for_dto=$(echo "$group" | sed 's/s$//' | awk '{print toupper(substr($0,0,1))substr($0,2)}')
|
||||
local dto_name="$(tr '[:lower:]' '[:upper:]' <<< ${action:0:1})${action:1}${resource_name_for_dto}Dto"
|
||||
dtos[$dto_name]=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Generate DTO interface definitions for TypeScript
|
||||
if [ "$format" == "ts" ]; then
|
||||
for dto in $(echo "${!dtos[@]}" | tr ' ' '\n' | sort); do
|
||||
dto_definitions+="export interface ${dto} {}\n"
|
||||
done
|
||||
dto_definitions+="\n"
|
||||
fi
|
||||
|
||||
# Sort the group names alphabetically to ensure consistent output
|
||||
sorted_groups=$(for group in "${!groups[@]}"; do echo "$group"; done | sort)
|
||||
|
||||
for group in $sorted_groups; do
|
||||
client_object+=" ${group}: {\n"
|
||||
|
||||
# Sort the lines within the group by the action name (field 3)
|
||||
sorted_lines=$(echo -e "${groups[$group]}" | sed '/^$/d' | sort -k3)
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [ -z "$line" ]; then continue; fi
|
||||
method=$(echo "$line" | cut -d' ' -f1)
|
||||
uri=$(echo "$line" | cut -d' ' -f2)
|
||||
action=$(echo "$line" | cut -d' ' -f3)
|
||||
|
||||
params=$(echo "$uri" | grep -o '{[^}]*}' | sed 's/[{}]//g')
|
||||
ts_types=""
|
||||
js_args=""
|
||||
|
||||
# Generate arguments for the function signature
|
||||
for p in $params; do
|
||||
js_args+="${p}, "
|
||||
ts_types+="${p}: number, "
|
||||
done
|
||||
|
||||
# Add a 'data' argument for POST/PUT/PATCH methods
|
||||
if [[ "$method" == "POST" || "$method" == "PUT" || "$method" == "PATCH" ]]; then
|
||||
local resource_name_for_dto=$(echo "$group" | sed 's/s$//' | awk '{print toupper(substr($0,0,1))substr($0,2)}')
|
||||
local dto_name="$(tr '[:lower:]' '[:upper:]' <<< ${action:0:1})${action:1}${resource_name_for_dto}Dto"
|
||||
ts_types+="data: ${dto_name}"
|
||||
js_args+="data"
|
||||
fi
|
||||
|
||||
# Clean up function arguments string
|
||||
func_args=$(echo "$ts_types" | sed 's/,\s*$//' | sed 's/,$//')
|
||||
js_args=$(echo "$js_args" | sed 's/,\s*$//' | sed 's/,$//')
|
||||
|
||||
final_args=$([ "$format" == "ts" ] && echo "$func_args" || echo "$js_args")
|
||||
|
||||
# Construct the fetch call string
|
||||
fetch_uri="/api/${uri}"
|
||||
fetch_uri=$(echo "$fetch_uri" | sed 's/{/${/g')
|
||||
|
||||
client_object+=" ${action}: (${final_args}) => fetch(\`${fetch_uri}\`"
|
||||
|
||||
# Add request options for non-GET methods
|
||||
if [ "$method" != "GET" ]; then
|
||||
client_object+=", {\n method: '${method}'"
|
||||
if [[ "$method" == "POST" || "$method" == "PUT" || "$method" == "PATCH" ]]; then
|
||||
client_object+=", \n body: JSON.stringify(data)"
|
||||
fi
|
||||
client_object+="\n }"
|
||||
fi
|
||||
client_object+="),\n"
|
||||
|
||||
done <<< "$sorted_lines"
|
||||
client_object+=" },\n"
|
||||
done
|
||||
|
||||
client_object+="};"
|
||||
|
||||
echo -e "// Generated from ${routes_file}\n" > "$outfile"
|
||||
echo -e "${dto_definitions}${client_object}" >> "$outfile"
|
||||
echo "API client generated at ${outfile}"
|
||||
}
|
||||
|
||||
# Function to generate OpenAPI spec
|
||||
generate_openapi() {
|
||||
local outfile=$1
|
||||
local paths_json=""
|
||||
|
||||
declare -A paths
|
||||
while read -r method uri action; do
|
||||
path="/api/${uri}"
|
||||
# OpenAPI uses lowercase methods
|
||||
method_lower=$(echo "$method" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Group operations by path
|
||||
if [[ -z "${paths[$path]}" ]]; then
|
||||
paths[$path]=""
|
||||
fi
|
||||
paths[$path]+="\"${method_lower}\": {\"summary\": \"${action}\"},"
|
||||
done
|
||||
|
||||
# Assemble the paths object
|
||||
sorted_paths=$(for path in "${!paths[@]}"; do echo "$path"; done | sort)
|
||||
for path in $sorted_paths; do
|
||||
operations=$(echo "${paths[$path]}" | sed 's/,$//') # remove trailing comma
|
||||
paths_json+="\"${path}\": {${operations}},"
|
||||
done
|
||||
paths_json=$(echo "$paths_json" | sed 's/,$//') # remove final trailing comma
|
||||
|
||||
# Create the final OpenAPI JSON structure
|
||||
openapi_spec=$(cat <<EOF
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "API Client",
|
||||
"version": "1.0.0",
|
||||
"description": "Generated from ${routes_file}"
|
||||
},
|
||||
"paths": {
|
||||
${paths_json}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$openapi_spec" > "$outfile"
|
||||
echo "OpenAPI spec generated at ${outfile}"
|
||||
}
|
||||
|
||||
|
||||
# Main logic
|
||||
parsed_routes=$(parse_routes "$routes_file")
|
||||
|
||||
if [[ "$output_format" == "ts" || "$output_format" == "js" ]]; then
|
||||
generate_client "$output_format" "$output_file" <<< "$parsed_routes"
|
||||
elif [[ "$output_format" == "openapi" ]]; then
|
||||
generate_openapi "$output_file" <<< "$parsed_routes"
|
||||
else
|
||||
echo "Invalid output format specified." >&2
|
||||
exit 1
|
||||
fi
|
||||
Loading…
Add table
Reference in a new issue