veylant/internal/routing/store.go
2026-02-23 13:35:04 +01:00

105 lines
2.9 KiB
Go

package routing
import (
"context"
"fmt"
"sort"
"sync"
"time"
)
// RuleStore is the persistence interface for routing rules.
// The MemStore implements it for tests; PgStore implements it for production.
type RuleStore interface {
// ListActive returns all enabled rules for tenantID, sorted by priority ASC.
ListActive(ctx context.Context, tenantID string) ([]RoutingRule, error)
// Get returns a single rule by id, scoped to tenantID.
Get(ctx context.Context, id, tenantID string) (RoutingRule, error)
// Create persists a new rule and returns the created record.
Create(ctx context.Context, rule RoutingRule) (RoutingRule, error)
// Update replaces an existing rule and returns the updated record.
Update(ctx context.Context, rule RoutingRule) (RoutingRule, error)
// Delete permanently removes a rule. Only affects the given tenantID.
Delete(ctx context.Context, id, tenantID string) error
}
// ErrNotFound is returned by Get/Update/Delete when the rule does not exist.
var ErrNotFound = fmt.Errorf("routing rule not found")
// ─── MemStore — in-memory implementation (tests & dev) ──────────────────────
// MemStore is a thread-safe in-memory RuleStore for unit tests.
type MemStore struct {
mu sync.RWMutex
rules map[string]RoutingRule // id → rule
seq int
}
// NewMemStore creates an empty MemStore.
func NewMemStore() *MemStore {
return &MemStore{rules: make(map[string]RoutingRule)}
}
func (m *MemStore) ListActive(_ context.Context, tenantID string) ([]RoutingRule, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var out []RoutingRule
for _, r := range m.rules {
if r.TenantID == tenantID && r.IsEnabled {
out = append(out, r)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Priority < out[j].Priority })
return out, nil
}
func (m *MemStore) Get(_ context.Context, id, tenantID string) (RoutingRule, error) {
m.mu.RLock()
defer m.mu.RUnlock()
r, ok := m.rules[id]
if !ok || r.TenantID != tenantID {
return RoutingRule{}, ErrNotFound
}
return r, nil
}
func (m *MemStore) Create(_ context.Context, rule RoutingRule) (RoutingRule, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.seq++
rule.ID = fmt.Sprintf("mem-%d", m.seq)
rule.CreatedAt = time.Now()
rule.UpdatedAt = time.Now()
m.rules[rule.ID] = rule
return rule, nil
}
func (m *MemStore) Update(_ context.Context, rule RoutingRule) (RoutingRule, error) {
m.mu.Lock()
defer m.mu.Unlock()
existing, ok := m.rules[rule.ID]
if !ok || existing.TenantID != rule.TenantID {
return RoutingRule{}, ErrNotFound
}
rule.CreatedAt = existing.CreatedAt
rule.UpdatedAt = time.Now()
m.rules[rule.ID] = rule
return rule, nil
}
func (m *MemStore) Delete(_ context.Context, id, tenantID string) error {
m.mu.Lock()
defer m.mu.Unlock()
r, ok := m.rules[id]
if !ok || r.TenantID != tenantID {
return ErrNotFound
}
delete(m.rules, id)
return nil
}