210 lines
5.2 KiB
Go
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()
|
|
}
|