feat(api): add --json, --json-fields, --jq to fj api
`fj api` was the only command that returned raw API JSON without exposing
the same projection/filtering knobs that `fj repo list`, `fj pr list`,
etc. already provide. Callers had to pipe to `python -m json.tool` or
`jq` to extract fields, which is inconsistent and discoverable only
after hitting the gap.
Wire the existing addJSONFlags / wantJSON / outputJSON helpers from
cmd/json.go so the API command participates in the same JSON output
pipeline. No behavioral change when none of the new flags are set —
default still pretty-prints JSON and writes raw bytes for non-JSON
responses.
Verified against live forgejo:
$ fj api repos/public/claude-code-proxy --jq .full_name
public/claude-code-proxy
$ fj api repos/public/claude-code-proxy --json=full_name,description
{ "description": "...", "full_name": "public/claude-code-proxy" }
$ fj api 'repos/public/claude-code-proxy/commits?limit=3' \
--jq '.[] | "\(.sha[0:8]) \(.commit.message | split("\n")[0])"'
8e550b97 Local fork: hardening + ops improvements ...
b9da198e Harden proxy auth, storage, and conversation access
6cda3631 Harden streaming, pagination, and config loading
Note: `--json=fields` requires the equals sign because the flag has
NoOptDefVal=" " (so `--json` alone is valid for "everything as JSON").
The Example block in --help documents both the `--json=` form and the
`--json-fields` alias which doesn't have that quirk.
This commit is contained in:
parent
0fda0b8679
commit
f75b831a53
1 changed files with 31 additions and 7 deletions
38
cmd/api.go
38
cmd/api.go
|
|
@ -35,7 +35,17 @@ If --field is used and no --method is specified, the method defaults to POST.`,
|
||||||
fj api /users/johndoe
|
fj api /users/johndoe
|
||||||
|
|
||||||
# Use raw body from stdin
|
# Use raw body from stdin
|
||||||
echo '{"title":"test"}' | fj api /repos/{owner}/{repo}/issues --input -`,
|
echo '{"title":"test"}' | fj api /repos/{owner}/{repo}/issues --input -
|
||||||
|
|
||||||
|
# Filter the response with a jq expression
|
||||||
|
fj api /repos/{owner}/{repo}/issues --jq '.[].title'
|
||||||
|
|
||||||
|
# Project the response down to specific fields (requires "=" because --json
|
||||||
|
# also accepts an empty value to mean "all fields as JSON")
|
||||||
|
fj api /repos/{owner}/{repo} --json=full_name,description,private
|
||||||
|
|
||||||
|
# Same projection without the "=" quirk
|
||||||
|
fj api /repos/{owner}/{repo} --json-fields full_name,description,private`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: runAPI,
|
RunE: runAPI,
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +60,7 @@ func init() {
|
||||||
apiCmd.Flags().StringArrayP("header", "H", nil, "Add an HTTP request header (key:value)")
|
apiCmd.Flags().StringArrayP("header", "H", nil, "Add an HTTP request header (key:value)")
|
||||||
apiCmd.Flags().Bool("silent", false, "Do not print the response body")
|
apiCmd.Flags().Bool("silent", false, "Do not print the response body")
|
||||||
apiCmd.Flags().BoolP("include", "i", false, "Include HTTP response headers in the output")
|
apiCmd.Flags().BoolP("include", "i", false, "Include HTTP response headers in the output")
|
||||||
|
addJSONFlags(apiCmd, "Output the response as JSON; pass a comma-separated field list to project specific keys")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runAPI(cmd *cobra.Command, args []string) error {
|
func runAPI(cmd *cobra.Command, args []string) error {
|
||||||
|
|
@ -212,18 +223,31 @@ func runAPI(cmd *cobra.Command, args []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pretty-print JSON, or output raw if not JSON
|
|
||||||
contentType := resp.Header.Get("Content-Type")
|
contentType := resp.Header.Get("Content-Type")
|
||||||
if strings.Contains(contentType, "json") || json.Valid(respBody) {
|
isJSON := strings.Contains(contentType, "json") || json.Valid(respBody)
|
||||||
|
|
||||||
|
// If the user asked for JSON projection or jq filtering, route through
|
||||||
|
// the shared JSON output helpers so the API command is consistent with
|
||||||
|
// `fj repo list`, `fj pr list`, etc.
|
||||||
|
if wantJSON(cmd) {
|
||||||
|
if !isJSON {
|
||||||
|
return fmt.Errorf("--json/--json-fields/--jq requires a JSON response, but the server returned %s", contentType)
|
||||||
|
}
|
||||||
|
var parsed any
|
||||||
|
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
||||||
|
return fmt.Errorf("response is not valid JSON: %w", err)
|
||||||
|
}
|
||||||
|
return outputJSON(cmd, parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pretty-print JSON by default, otherwise emit raw bytes.
|
||||||
|
if isJSON {
|
||||||
var parsed any
|
var parsed any
|
||||||
if err := json.Unmarshal(respBody, &parsed); err == nil {
|
if err := json.Unmarshal(respBody, &parsed); err == nil {
|
||||||
enc := json.NewEncoder(ios.Out)
|
return writeJSON(parsed)
|
||||||
enc.SetIndent("", " ")
|
|
||||||
return enc.Encode(parsed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Raw output for non-JSON responses
|
|
||||||
_, err = ios.Out.Write(respBody)
|
_, err = ios.Out.Write(respBody)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue