Skip to content

feat(api): add /v1/analyze handler and routing#14

Merged
hidetzu merged 1 commit into
mainfrom
feat/api-analyze-endpoint
Apr 11, 2026
Merged

feat(api): add /v1/analyze handler and routing#14
hidetzu merged 1 commit into
mainfrom
feat/api-analyze-endpoint

Conversation

@hidetzu
Copy link
Copy Markdown
Owner

@hidetzu hidetzu commented Apr 11, 2026

Summary

PR 8/9 of the Phase 2 series. First user-facing endpoint. Wires the analyze usecase adapter (PR 7) behind `POST /v1/analyze`: request decoding, validation, error mapping, and routing in `app.New()`. The defensive middleware chain from PR 6 is already in place, so the new endpoint automatically inherits request_id, recover, logging, body_limit, rate_limit, concurrency_limit, and timeout.

Handler design

Interface defined in the handler package

`AnalyzeUsecase` is defined in the handler package, not imported from `internal/usecase`. This is the "accept interfaces, return structs" idiom mandated by `docs/development_rules.md` §10. The concrete `usecase.Analyzer` satisfies the interface automatically because Go uses structural typing. A test fake in the same test file is enough to exercise every handler branch.

type AnalyzeUsecase interface {
    Analyze(ctx context.Context, in usecase.AnalyzeInput) (prism.Result, error)
}

Zero translation from `pkg/prism.Result` to wire format

`pkg/prism.Result` already has the canonical JSON tags (`pull_request`, `analysis`, `changed_files`) — the same shape required by Phase 2 instruction §7. The handler serializes it directly in the envelope:

response.WriteJSON(w, 200, map[string]any{\"result\": result})

No DTO, no translation layer. This matches the instruction's "HTTP 層で独自 shape に変換しすぎない" guidance.

Error mapping

Three layers of error handling in the handler:

Source Maps to HTTP
JSON decode failure `CodeInvalidInput` 400
`*http.MaxBytesError` from `body_limit`'s `MaxBytesReader` `CodePayloadTooLarge` 413
`validation.Error` `CodeInvalidInput` with `field: message` 400
`prism.ErrInvalidInput` `CodeInvalidInput` 400
`prism.ErrUnsupportedProvider` `CodeUnsupportedProvider` 400
`prism.ErrAuthRequired` `CodeAuthRequired` 401
`prism.ErrUpstreamFailure` `CodeUpstreamFailure` 502
unknown `CodeInternalError` 500

Client-facing messages are fixed strings, never the raw pkg/prism error text, so implementation details never leak to users. This is the right default — we can always add more specific messages later if operational feedback demands it.

MaxBytesError handling

The body_limit middleware (PR 3) wraps `r.Body` with `http.MaxBytesReader` but read-time enforcement requires handler cooperation — that was flagged in PR 3's commit message. This PR fulfills that contract: the handler's `json.Decode` detects `*http.MaxBytesError` via `errors.As` and returns 413 instead of 400, matching the early-rejection path.

Routing

analyzeHandler := handler.NewAnalyzeHandler(usecase.NewAnalyzer())
mux.HandleFunc(\"POST /v1/analyze\", analyzeHandler.Handle)

3 lines added to `app.New()`. The existing middleware chain and health endpoints are untouched.

Test plan

13 tests across 6 groups, all using a fake usecase in the same package to keep handler tests independent of `pkg/prism`.

Test Verifies
`Success` 200 with envelope `{"result": {pull_request, analysis, changed_files}}` fully populated; usecase received the correct `AnalyzeInput`
`InvalidJSON` Malformed JSON → 400 `invalid_input`, usecase not invoked
`MissingPullRequestURL` Empty body `{}` → 400 `invalid_input` with message mentioning `pull_request_url`
`InvalidPullRequestURL` (×4 sub-tests) http scheme / wrong host / issues path / missing PR number → 400
`BodyTooLarge` Simulated `MaxBytesReader` trip → 413 `payload_too_large`, usecase not invoked
`UsecaseErrorMapping` (×5 sub-tests) All four pkg/prism sentinels + unknown error map to the correct `response.Code` + HTTP status
`AnalyzeRequest_Validate` (×4 sub-tests) Unit test of `Validate()` alone — valid / empty / whitespace / non-github

Local checks

  • `go vet ./...` clean
  • `go test ./... -race -count=1` all pass (13 new + all existing)
  • `golangci-lint run` 0 issues
  • `go mod tidy` clean (no new dependencies beyond PR 7's pkg/prism)

What this PR does NOT do

  • No Fly.io smoke test in this PR. Per our conversation, interim Fly verifications are deferred to v0.2.0 release time. Unit + handler tests are sufficient to land the PR.
  • No README update. `/v1/analyze` documentation will be added as part of the v0.2.0 release cleanup.
  • No `/v1/prompt`. That's PR 9, which mirrors this PR's structure for the prompt endpoint.

Phase 2 context

# PR Status
1 response codes merged
2 validation helpers merged
3 body_limit middleware merged
4 rate_limit middleware merged
5 concurrency_limit middleware merged
6 wire defensive middleware + E2E test merged (Fly verified)
7 analyze usecase adapter merged
8 /v1/analyze handler and routing (this PR) open
9 /v1/prompt endpoint blocked on 7

Intended to be squash-merged.

First user-facing Phase 2 endpoint. Wires the analyze usecase
adapter (PR 7) behind POST /v1/analyze, with JSON decoding,
request validation, error mapping, and routing.

Handler design:

- AnalyzeRequest holds pull_request_url and has a Validate()
  method that chains validation.Required and
  validation.GitHubPullRequestURL. Request-type-owned validation
  matches development_rules.md §11.
- AnalyzeUsecase is an interface defined in the handler package,
  not imported from internal/usecase. The concrete
  usecase.Analyzer satisfies it, following the "accept
  interfaces, return structs" idiom per §10. This keeps handler
  tests independent of pkg/prism — a fake usecase in the same
  test file is enough.
- The success response envelope is {"result": prism.Result}.
  pkg/prism.Result already carries the canonical JSON tags
  (pull_request, analysis, changed_files) so no translation
  layer is needed between the usecase output and the wire
  format, matching Phase 2 instruction §7.
- Error handling maps three categories of failure to the
  standard error envelope:
    1. JSON decode errors → CodeInvalidInput (400)
    2. *http.MaxBytesError from the body_limit middleware's
       MaxBytesReader → CodePayloadTooLarge (413). Handlers
       are responsible for surfacing read-time rejection per
       PR 3's design note.
    3. validation.Error → CodeInvalidInput with the structured
       field: message format
    4. pkg/prism sentinel errors → matching response.Code:
         ErrInvalidInput         → invalid_input (400)
         ErrUnsupportedProvider  → unsupported_provider (400)
         ErrAuthRequired         → auth_required (401)
         ErrUpstreamFailure      → upstream_failure (502)
         unknown                 → internal_error (500)
- Client-facing error messages are fixed strings, not the raw
  pkg/prism error text, so implementation details never leak.

Routing:

- app.New() constructs usecase.NewAnalyzer() and
  handler.NewAnalyzeHandler() and registers POST /v1/analyze on
  the existing mux. The middleware chain from PR 6 already wraps
  all routes, so the new endpoint gets request_id, recover,
  logging, body_limit, rate_limit, concurrency_limit, and
  timeout automatically.

Tests:

- Success path verifies the 200 response is exactly
  {"result": {pull_request, analysis, changed_files}} with the
  fake usecase's canned pkg/prism.Result serialized through.
- Invalid JSON, missing pull_request_url, and four malformed
  URL shapes all return 400 with invalid_input and a non-empty
  message.
- Body too large is simulated by wrapping the request body in
  http.MaxBytesReader directly; the handler must surface the
  *http.MaxBytesError as 413 payload_too_large.
- Usecase error mapping covers all four pkg/prism sentinels
  plus an unknown error (→ internal_error / 500).
- AnalyzeRequest.Validate() has its own unit test for the valid,
  empty, whitespace-only, and non-github URL cases.
@hidetzu hidetzu merged commit f8ff63e into main Apr 11, 2026
2 checks passed
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.

1 participant