Skip to content

feat(api): ana api raw-JSON passthrough verb (#25)#26

Merged
bradfair merged 4 commits into
mainfrom
feature/ana-api-verb
Apr 23, 2026
Merged

feat(api): ana api raw-JSON passthrough verb (#25)#26
bradfair merged 4 commits into
mainfrom
feature/ana-api-verb

Conversation

@bradfair
Copy link
Copy Markdown
Contributor

@bradfair bradfair commented Apr 23, 2026

Closes #25.

Summary

  • ana api <path> — authenticated raw-JSON passthrough. Leading-slash path = verbatim (covers /v1/... REST and pre-resolved /rpc/public/...); no leading slash = Connect-RPC short form (textql.rpc.public.<Service>/<Method>, prefixed with /rpc/public/).
  • Flags: --method (default POST), --data/--data-stdin (mutually exclusive), --raw (skip pretty-print). GET/HEAD omit the default body; other methods default to {} so short-form RPC calls Just Work. Non-2xx bodies land on stderr so stdout stays | jq-clean.
  • Transport: added DoRaw, and bearer + User-Agent moved to a bearerTransport RoundTripper so Unary/Stream/DoRaw share one auth path.
  • --data "" is treated like no body (no Content-Type, no zero-byte request body).

Coverage stays at 100% on ./internal/....

Summary by CodeRabbit

  • New Features

    • Added a top-level api CLI command to make authenticated HTTP requests with a required .
    • Flags: --method, --data, --data-stdin (mutually exclusive), and --raw passthrough.
    • Short-form RPC paths auto-prefix to public RPC endpoints; leading-slash paths passed through.
  • Improvements

    • Authentication and User-Agent consistently applied to outbound requests.
    • Non-2xx responses surface HTTP code and server body on stderr; 2xx responses are pretty-printed JSON by default.
  • Tests

    • Expanded tests covering command behavior, argument parsing, error handling, and transport behavior.

Thin authenticated HTTP passthrough. Short-form Connect-RPC
(`textql.rpc.public.<Service>/<Method>`) and documented REST
(`/v1/...`) distinguished by leading slash. Default method POST with
`{}` body so RPC shorthand works out of the box; `--data`/`--data-stdin`
supply a custom body, `--raw` skips pretty-print. Non-2xx bodies go to
stderr so stdout stays `| jq`-clean.

Transport gains `DoRaw` and moves bearer/UA injection to a
`bearerTransport` RoundTripper so Unary/Stream/DoRaw share one auth
path with no per-call-site header plumbing. Coverage stays at 100%
on `./internal/...`.
`--data ""` at the `ana api` layer previously sent Content-Type:
application/json with a zero-byte request body. That's semantically
odd (empty-but-JSON-tagged) and different from both "no --data"
(defaults to `{}`) and `--data '{}'` (literal empty object). Guard on
`len(body) > 0` in both the body-reader and Content-Type branches so
empty-slice and nil behave identically — no body, no Content-Type.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dceeb84f-3802-4ad0-9293-cb0ff4f0594b

📥 Commits

Reviewing files that changed from the base of the PR and between 754b1ac and 84410a0.

📒 Files selected for processing (2)
  • internal/api/call.go
  • internal/api/call_test.go

📝 Walkthrough

Walkthrough

Adds an ana api CLI verb for authenticated raw-JSON calls, a new internal/api verb package implementing path/method/body/--raw semantics, and transport-layer changes: a bearerTransport RoundTripper and Client.DoRaw to centralize Authorization/User-Agent injection and raw HTTP passthrough.

Changes

Cohort / File(s) Summary
CLI wiring
cmd/ana/main.go, cmd/ana/main_test.go
Register new top-level api verb and update verb-shape test to include "api".
API verb package
internal/api/api.go, internal/api/api_test.go, internal/api/call.go, internal/api/call_test.go
Add api.New(Deps) and call command: path resolution (prefix non-leading-slash with /rpc/public/), flags --method, --data, --data-stdin (mutually exclusive), --raw, request body resolution, DoRaw invocation, stdout/stderr handling for 2xx vs non-2xx, and comprehensive tests.
Transport client
internal/transport/client.go, internal/transport/client_test.go
Move per-request auth into bearerTransport RoundTripper installed in New, clone supplied http.Client, add DoRaw(ctx, method, path, body) (int, []byte, error), and extend tests to cover auth wrapping, header preservation, error paths, and DoRaw behaviors.

Sequence Diagram

sequenceDiagram
    participant User as CLI User
    participant Cmd as ana api
    participant Client as transport.Client
    participant Transport as bearerTransport
    participant HTTP as net/http Transport
    participant Server as API Server

    User->>Cmd: ana api <path> [--method/--data/--raw]
    Cmd->>Client: request via DoRaw (method,path,body)
    Client->>Transport: RoundTrip(enriched http.Request)
    Transport->>Transport: tokenFn(ctx) -> token\nset Authorization & User-Agent (if missing)
    Transport->>HTTP: RoundTrip(request)
    HTTP->>Server: HTTP request
    Server-->>HTTP: HTTP response (status + body)
    HTTP->>Transport: Response
    Transport->>Client: Response
    Client->>Cmd: (status, body) or error
    alt status 2xx
        Cmd->>User: pretty-print JSON or write raw bytes to stdout
    else non-2xx
        Cmd->>User: write body to stderr and return error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped in, sniffed the API gate,
Auth tucked in middleware — tidy and straight.
Paths get prefixed, JSON gets shown,
Or raw bytes pass through, all on their own.
Hop on, poke endpoints — the rabbit's proud and elate.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(api): ana api raw-JSON passthrough verb (#25)' clearly summarizes the main change: adding a new API verb for raw-JSON passthrough.
Description check ✅ Passed The description covers all template requirements: a clear summary of changes, the PR is marked as 'feat', and it addresses lint/coverage/docs requirements.
Linked Issues check ✅ Passed The changeset fully implements issue #25 requirements: adds 'ana api ' verb with Connect-RPC/REST path support, implements --method/--data/--data-stdin/--raw flags, shares auth via bearerTransport, reuses existing config, and ensures errors go to stderr.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the 'ana api' verb as specified in issue #25; no out-of-scope additions detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ana-api-verb

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/api/call_test.go`:
- Around line 97-125: Add a new test that asserts an explicit empty --data
string produces an empty body (not the default {}), e.g. mirror
TestDataFlagUsedAsBody but call cmd.Run with []string{"--data", "", "foo/Bar"}
using the same fakeDeps, New(...) and stdio setup and then assert that
f.lastBody equals the empty string; place the test alongside
TestDataFlagUsedAsBody/TestDataStdinUsedAsBody so every body-resolution branch
(omitted, non-empty, empty, and stdin) is covered.

In `@internal/api/call.go`:
- Around line 63-66: The current check only ensures the first positional is
present but ignores any extra arguments (fs.NArg() and fs.Arg(0) usage), so
commands like `ana api /v1/things stray` silently drop extras; change the
validation to reject when there are extra positionals by returning a UsageErrf
if fs.NArg() != 1 (or if any additional fs.Arg(i) is non-blank), and only then
set path := fs.Arg(0); update the validation logic around fs.NArg() and
fs.Arg(0) to enforce exactly one non-blank positional argument.

In `@internal/transport/client.go`:
- Around line 84-97: The RoundTrip implementation mutates the incoming
*http.Request (violating net/http contract); fix by cloning the request before
injecting headers: in bearerTransport.RoundTrip, call cloned :=
req.Clone(req.Context()) (ensure cloned.Header is non-nil) and modify
cloned.Header (set User-Agent and Authorization using b.c.userAgent and
b.c.tokenFn) and then call b.next.RoundTrip(cloned) instead of modifying req in
place; keep the existing token error handling and header-presence checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ed0b9c97-7cab-4308-8bd6-b7faa7725a88

📥 Commits

Reviewing files that changed from the base of the PR and between ed40e1a and 5f41346.

⛔ Files ignored due to path filters (5)
  • README.md is excluded by !**/*.md
  • docs/features.md is excluded by !**/docs/**, !**/*.md
  • internal/CLAUDE.md is excluded by !**/*.md
  • internal/api/CLAUDE.md is excluded by !**/*.md
  • internal/transport/CLAUDE.md is excluded by !**/*.md
📒 Files selected for processing (8)
  • cmd/ana/main.go
  • cmd/ana/main_test.go
  • internal/api/api.go
  • internal/api/api_test.go
  • internal/api/call.go
  • internal/api/call_test.go
  • internal/transport/client.go
  • internal/transport/client_test.go

Comment thread internal/api/call_test.go
Comment thread internal/api/call.go Outdated
Comment thread internal/transport/client.go Outdated
- transport: bearerTransport.RoundTrip now clones the incoming request
  before mutating headers, per the net/http RoundTripper contract (must
  not modify the request except for consuming/closing the body).
- api: reject extra positional args — `ana api /v1/x stray` was silently
  dropping `stray`, masking typos where the user probably meant a flag.
- api: pin the `--data ""` contract with a test so the empty-string
  explicit body can't drift back to the POST default {}.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
internal/api/call.go (1)

63-69: ⚠️ Potential issue | 🟡 Minor

Dispatch the trimmed path, not the raw positional.

Line 63 validates strings.TrimSpace(fs.Arg(0)), but Line 69 still forwards the untrimmed token. ana api " /v1/things " will therefore pass validation and then hit a different URL than the user intended.

✂️ Proposed fix
 	if fs.NArg() == 0 || strings.TrimSpace(fs.Arg(0)) == "" {
 		return cli.UsageErrf("api: <path> positional argument required")
 	}
 	if fs.NArg() > 1 {
 		return cli.UsageErrf("api: unexpected positional arguments: %v", fs.Args()[1:])
 	}
-	path := fs.Arg(0)
+	path := strings.TrimSpace(fs.Arg(0))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/call.go` around lines 63 - 69, The code validates the positional
arg with strings.TrimSpace(fs.Arg(0)) but then assigns the raw token to path;
change the assignment so path = strings.TrimSpace(fs.Arg(0)) (or assign a
trimmed variable before use) and ensure all subsequent uses (the variable path
in call dispatch code) use that trimmed value so an input like " /v1/things " is
dispatched to the trimmed URL; update any references to fs.Arg(0) in the
surrounding function (e.g., where path is used) to use the new trimmed variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/api/call.go`:
- Around line 63-69: The code validates the positional arg with
strings.TrimSpace(fs.Arg(0)) but then assigns the raw token to path; change the
assignment so path = strings.TrimSpace(fs.Arg(0)) (or assign a trimmed variable
before use) and ensure all subsequent uses (the variable path in call dispatch
code) use that trimmed value so an input like " /v1/things " is dispatched to
the trimmed URL; update any references to fs.Arg(0) in the surrounding function
(e.g., where path is used) to use the new trimmed variable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a7b455af-7f2c-4f52-8b5d-faeeca1a1f93

📥 Commits

Reviewing files that changed from the base of the PR and between 5f41346 and 754b1ac.

📒 Files selected for processing (4)
  • internal/api/call.go
  • internal/api/call_test.go
  • internal/transport/client.go
  • internal/transport/client_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 23, 2026
A whitespace-padded positional like `ana api " /v1/things "` previously
passed the blank-check (which ran on the trimmed view) but got forwarded
verbatim to the transport, where joinURL would stitch it into a
malformed URL. Trim once, reuse, and re-check against empty so the
single source of truth for `path` is the trimmed string.
@bradfair bradfair added this pull request to the merge queue Apr 23, 2026
Merged via the queue into main with commit b16cadf Apr 23, 2026
10 checks passed
@bradfair bradfair deleted the feature/ana-api-verb branch April 23, 2026 15:34
@hpt-bot hpt-bot Bot mentioned this pull request Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(api): add ana api raw-JSON verb (analogous to gh api)

1 participant