rdev/pkg/api/decode.go
jordan 8282d60c69 feat: implement composable monorepo template system with component architecture
Adds the composable monorepo template system that generates project skeletons
with pluggable components (service, worker, app-react, app-astro, cli).

Key changes:
- Monorepo skeleton templates with shared pkg/, scripts/, and git hooks
- Component templates (service, worker, app-react, app-astro, cli) with
  Dockerfiles, CI steps, and component.yaml manifests
- Component domain model with validation and dependency resolution
- Component handler endpoints for CRUD and composition
- Template provider extended with BuildComposableProject and component assembly
- Deployer extended with composable project deployment support
- Handler timeout constants (TimeoutFastLookup through TimeoutLongRunning)
- envutil package for centralized env var reads with defaults
- api.DecodeJSON helper for standardized request body decoding
- Standardized response helpers (WriteBadRequest, WriteNotFound, etc.)
- Replaced fullstack-app cookbook with composable-app cookbook
- Hardened handler timeouts, logging, and error responses across all handlers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:11:42 -07:00

64 lines
1.4 KiB
Go

package api
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
var (
// ErrEmptyBody is returned when the request body is empty or nil.
ErrEmptyBody = errors.New("request body is empty")
// ErrInvalidJSON is returned when the request body contains invalid JSON.
ErrInvalidJSON = errors.New("invalid JSON")
)
// DecodeJSON decodes JSON from the request body into v.
// Returns descriptive errors for common failure cases:
// - nil or empty body → ErrEmptyBody
// - malformed JSON → ErrInvalidJSON
//
// Usage:
//
// if err := api.DecodeJSON(r, &req); err != nil {
// api.WriteBadRequest(w, r, "invalid request body")
// return
// }
func DecodeJSON(r *http.Request, v any) error {
if r.Body == nil {
return ErrEmptyBody
}
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(v); err != nil {
if errors.Is(err, io.EOF) {
return ErrEmptyBody
}
return fmt.Errorf("%w: %w", ErrInvalidJSON, err)
}
return nil
}
// DecodeJSONStrict decodes JSON from the request body into v.
// Rejects JSON containing fields not present in the target struct.
func DecodeJSONStrict(r *http.Request, v any) error {
if r.Body == nil {
return ErrEmptyBody
}
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(v); err != nil {
if errors.Is(err, io.EOF) {
return ErrEmptyBody
}
return fmt.Errorf("%w: %w", ErrInvalidJSON, err)
}
return nil
}