Files
abuse-registration-api-golang/internal/store/memory.go
T

210 lines
5.2 KiB
Go

package store
import (
"math/rand"
"sort"
"strings"
"sync"
"time"
"abuse_registration_poc/internal/models"
"abuse_registration_poc/internal/utils"
)
type MemoryStore struct {
mu sync.RWMutex
rows []models.Registration
resetInterval time.Duration
lastReset time.Time
nextReset time.Time
baseSize int
}
func NewMemoryStore(baseSize int, resetInterval time.Duration) *MemoryStore {
if baseSize < 1 {
baseSize = 546
}
if resetInterval < time.Second {
resetInterval = 10 * time.Minute
}
s := &MemoryStore{baseSize: baseSize, resetInterval: resetInterval}
s.Reset()
return s
}
func (s *MemoryStore) ResetInterval() time.Duration {
return s.resetInterval
}
func (s *MemoryStore) Reset() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().UTC()
s.rows = generateBaseDataset(s.baseSize)
s.lastReset = now
s.nextReset = now.Add(s.resetInterval)
}
func (s *MemoryStore) Snapshot() ([]models.Registration, time.Time, time.Time) {
s.resetIfNeeded()
s.mu.RLock()
defer s.mu.RUnlock()
rows := make([]models.Registration, len(s.rows))
copy(rows, s.rows)
return rows, s.lastReset, s.nextReset
}
func (s *MemoryStore) List(filter models.RegistrationFilter) []models.Registration {
rows, _, _ := s.Snapshot()
return applyFilter(rows, filter)
}
func (s *MemoryStore) Get(id string) (models.Registration, bool) {
s.resetIfNeeded()
s.mu.RLock()
defer s.mu.RUnlock()
for _, row := range s.rows {
if row.ID == id {
return row, true
}
}
return models.Registration{}, false
}
func (s *MemoryStore) Create(input models.Registration) (models.Registration, error) {
s.resetIfNeeded()
if err := models.NormalizeRegistration(&input); err != nil {
return models.Registration{}, err
}
s.mu.Lock()
defer s.mu.Unlock()
input.ID = utils.NewUUID()
s.rows = append(s.rows, input)
return input, nil
}
func (s *MemoryStore) Replace(id string, input models.Registration) (models.Registration, bool, error) {
s.resetIfNeeded()
if err := models.NormalizeRegistration(&input); err != nil {
return models.Registration{}, false, err
}
s.mu.Lock()
defer s.mu.Unlock()
for i := range s.rows {
if s.rows[i].ID == id {
input.ID = id
s.rows[i] = input
return input, true, nil
}
}
return models.Registration{}, false, nil
}
func (s *MemoryStore) Delete(id string) bool {
s.resetIfNeeded()
s.mu.Lock()
defer s.mu.Unlock()
for i := range s.rows {
if s.rows[i].ID == id {
s.rows = append(s.rows[:i], s.rows[i+1:]...)
return true
}
}
return false
}
func (s *MemoryStore) resetIfNeeded() {
s.mu.RLock()
due := time.Now().UTC().After(s.nextReset)
s.mu.RUnlock()
if due {
s.Reset()
}
}
func applyFilter(rows []models.Registration, filter models.RegistrationFilter) []models.Registration {
location := strings.ToLower(strings.TrimSpace(filter.Location))
abuseType := strings.ToLower(strings.TrimSpace(filter.AbuseType))
gender := strings.ToLower(strings.TrimSpace(filter.Gender))
status := strings.ToLower(strings.TrimSpace(filter.Status))
search := strings.ToLower(strings.TrimSpace(filter.Search))
out := make([]models.Registration, 0, len(rows))
for _, row := range rows {
if location != "" && strings.ToLower(row.Location) != location {
continue
}
if abuseType != "" && strings.ToLower(row.AbuseType) != abuseType {
continue
}
if gender != "" && strings.ToLower(row.Gender) != gender {
continue
}
if status != "" && strings.ToLower(row.Status) != status {
continue
}
if filter.HasFrom || filter.HasTo {
registeredAt, err := time.Parse(time.RFC3339, row.RegisteredAt)
if err != nil {
continue
}
if filter.HasFrom && registeredAt.Before(filter.From) {
continue
}
if filter.HasTo && registeredAt.After(filter.To) {
continue
}
}
if search != "" && !strings.Contains(strings.ToLower(row.ID+" "+row.Location+" "+row.AbuseType+" "+row.Gender+" "+row.Status), search) {
continue
}
out = append(out, row)
}
sort.SliceStable(out, func(i, j int) bool { return out[i].RegisteredAt < out[j].RegisteredAt })
if filter.Offset > 0 {
if filter.Offset >= len(out) {
return []models.Registration{}
}
out = out[filter.Offset:]
}
if filter.Limit > 0 && filter.Limit < len(out) {
out = out[:filter.Limit]
}
return out
}
func generateBaseDataset(count int) []models.Registration {
seed := rand.New(rand.NewSource(19881991))
locations := models.FaroeLocations
rows := make([]models.Registration, 0, count)
for i := 0; i < count; i++ {
location := locations[i%len(locations)]
if i >= len(locations) {
location = locations[seed.Intn(len(locations))]
}
rows = append(rows, models.Registration{
ID: utils.NewUUID(),
RegisteredAt: randomRegistrationTime(seed).Format(time.RFC3339),
Gender: models.Genders[seed.Intn(len(models.Genders))],
Location: location,
AbuseType: models.AbuseCategories[seed.Intn(len(models.AbuseCategories))],
Status: models.Statuses[seed.Intn(len(models.Statuses))],
})
}
return rows
}
func randomRegistrationTime(seed *rand.Rand) time.Time {
start := time.Date(1988, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(1991, 12, 31, 23, 59, 59, 0, time.UTC)
span := end.Unix() - start.Unix()
return time.Unix(start.Unix()+seed.Int63n(span), 0).UTC()
}