persona-community-5/services/persona-api/internal/adapter/memory/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

129 lines
3.1 KiB
Go

package memory
import (
"context"
"encoding/json"
"sort"
"sync"
"github.com/google/uuid"
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/domain"
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/port"
)
// Compile-time verification that PersonaRepository implements port.PersonaRepository.
var _ port.PersonaRepository = (*PersonaRepository)(nil)
// PersonaRepository is a thread-safe in-memory implementation of port.PersonaRepository.
type PersonaRepository struct {
mu sync.RWMutex
personas map[domain.PersonaID]*domain.Persona
handles map[string]domain.PersonaID
}
// NewPersonaRepository creates a new in-memory persona repository.
func NewPersonaRepository() *PersonaRepository {
return &PersonaRepository{
personas: make(map[domain.PersonaID]*domain.Persona),
handles: make(map[string]domain.PersonaID),
}
}
func (r *PersonaRepository) Create(_ context.Context, persona *domain.Persona) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.handles[persona.Handle]; exists {
return domain.ErrDuplicateHandle
}
if persona.ID.IsZero() {
persona.ID = domain.PersonaID(uuid.New().String())
}
r.personas[persona.ID] = copyPersona(persona)
r.handles[persona.Handle] = persona.ID
return nil
}
func (r *PersonaRepository) GetByID(_ context.Context, id domain.PersonaID) (*domain.Persona, error) {
r.mu.RLock()
defer r.mu.RUnlock()
p, ok := r.personas[id]
if !ok {
return nil, domain.ErrPersonaNotFound
}
return copyPersona(p), nil
}
func (r *PersonaRepository) List(_ context.Context, limit, offset int) ([]*domain.Persona, error) {
r.mu.RLock()
defer r.mu.RUnlock()
all := make([]*domain.Persona, 0, len(r.personas))
for _, p := range r.personas {
all = append(all, p)
}
sort.Slice(all, func(i, j int) bool {
return all[i].CreatedAt.After(all[j].CreatedAt)
})
if offset >= len(all) {
return []*domain.Persona{}, nil
}
end := offset + limit
if end > len(all) {
end = len(all)
}
result := make([]*domain.Persona, 0, end-offset)
for _, p := range all[offset:end] {
result = append(result, copyPersona(p))
}
return result, nil
}
func (r *PersonaRepository) Update(_ context.Context, persona *domain.Persona) error {
r.mu.Lock()
defer r.mu.Unlock()
existing, ok := r.personas[persona.ID]
if !ok {
return domain.ErrPersonaNotFound
}
if existing.Handle != persona.Handle {
if _, exists := r.handles[persona.Handle]; exists {
return domain.ErrDuplicateHandle
}
delete(r.handles, existing.Handle)
r.handles[persona.Handle] = persona.ID
}
r.personas[persona.ID] = copyPersona(persona)
return nil
}
func copyPersona(p *domain.Persona) *domain.Persona {
cp := *p
if p.Tags != nil {
cp.Tags = make([]string, len(p.Tags))
copy(cp.Tags, p.Tags)
}
if p.ImageURLs != nil {
cp.ImageURLs = make([]string, len(p.ImageURLs))
copy(cp.ImageURLs, p.ImageURLs)
}
if p.VideoURLs != nil {
cp.VideoURLs = make([]string, len(p.VideoURLs))
copy(cp.VideoURLs, p.VideoURLs)
}
if p.SpecJSON != nil {
cp.SpecJSON = make(json.RawMessage, len(p.SpecJSON))
copy(cp.SpecJSON, p.SpecJSON)
}
return &cp
}