persona-community-5/services/persona-api/internal/api/handlers/persona.go
rdev-worker 9c009926d1
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /implement-feature persona-model --requirements 'DB migration in pers...
2026-02-24 07:58:27 +00:00

152 lines
4.2 KiB
Go

package handlers
import (
"errors"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"git.threesix.ai/jordan/persona-community-5/pkg/app"
"git.threesix.ai/jordan/persona-community-5/pkg/httperror"
"git.threesix.ai/jordan/persona-community-5/pkg/httpresponse"
"git.threesix.ai/jordan/persona-community-5/pkg/logging"
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/domain"
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/service"
)
// Persona handles HTTP requests for persona CRUD operations.
type Persona struct {
svc *service.PersonaService
logger *logging.Logger
}
// NewPersona creates a new Persona handler with injected dependencies.
func NewPersona(svc *service.PersonaService, logger *logging.Logger) *Persona {
return &Persona{
svc: svc,
logger: logger.WithComponent("PersonaHandler"),
}
}
// CreatePersonaRequest is the request body for creating a persona.
type CreatePersonaRequest struct {
Description string `json:"description" validate:"required,min=3,max=1000"`
Gender string `json:"gender" validate:"required,oneof=woman man non_binary"`
CustomName string `json:"custom_name"`
}
// PersonaResponse is the API representation of a persona.
type PersonaResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Handle string `json:"handle"`
Gender string `json:"gender"`
Description string `json:"description"`
Tags []string `json:"tags"`
SpecJSON any `json:"spec_json,omitempty"`
AnchorURL string `json:"anchor_url,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
BannerURL string `json:"banner_url,omitempty"`
ImageURLs []string `json:"image_urls"`
VideoURLs []string `json:"video_urls"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
func toPersonaResponse(p *domain.Persona) PersonaResponse {
resp := PersonaResponse{
ID: p.ID.String(),
Name: p.Name,
Handle: p.Handle,
Gender: p.Gender,
Description: p.Description,
Tags: p.Tags,
AnchorURL: p.AnchorURL,
AvatarURL: p.AvatarURL,
BannerURL: p.BannerURL,
ImageURLs: p.ImageURLs,
VideoURLs: p.VideoURLs,
Status: string(p.Status),
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if p.SpecJSON != nil {
resp.SpecJSON = p.SpecJSON
}
return resp
}
// Create creates a new persona and enqueues a generate_spec job.
// Returns 202 Accepted with the created persona.
func (h *Persona) Create(w http.ResponseWriter, r *http.Request) error {
var req CreatePersonaRequest
if err := app.BindAndValidate(r, &req); err != nil {
return err
}
persona, err := h.svc.Create(r.Context(), req.Description, req.Gender, req.CustomName)
if err != nil {
return mapPersonaDomainError(err)
}
httpresponse.Accepted(w, r, toPersonaResponse(persona))
return nil
}
// GetByID returns a single persona by ID.
func (h *Persona) GetByID(w http.ResponseWriter, r *http.Request) error {
id := chi.URLParam(r, "id")
if id == "" {
return httperror.BadRequest("persona ID is required")
}
persona, err := h.svc.GetByID(r.Context(), domain.PersonaID(id))
if err != nil {
return mapPersonaDomainError(err)
}
httpresponse.OK(w, r, toPersonaResponse(persona))
return nil
}
// List returns a paginated list of personas.
func (h *Persona) List(w http.ResponseWriter, r *http.Request) error {
limit := 20
offset := 0
if v := r.URL.Query().Get("limit"); v != "" {
if parsed, err := strconv.Atoi(v); err == nil {
limit = parsed
}
}
if v := r.URL.Query().Get("offset"); v != "" {
if parsed, err := strconv.Atoi(v); err == nil {
offset = parsed
}
}
personas, err := h.svc.List(r.Context(), limit, offset)
if err != nil {
return err
}
results := make([]PersonaResponse, len(personas))
for i, p := range personas {
results[i] = toPersonaResponse(p)
}
httpresponse.OK(w, r, results)
return nil
}
func mapPersonaDomainError(err error) error {
switch {
case errors.Is(err, domain.ErrPersonaNotFound):
return httperror.NotFound("persona not found")
case errors.Is(err, domain.ErrDuplicateHandle):
return httperror.Conflict("persona with this handle already exists")
default:
return err
}
}