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>
64 lines
1.4 KiB
Go
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
|
|
}
|