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 } }