Nigiri/store.go
2025-08-15 16:37:17 -05:00

534 lines
13 KiB
Go

package nigiri
import (
"fmt"
"maps"
"reflect"
"sort"
"strings"
"sync"
)
// Validatable interface for entities that can validate themselves
type Validatable interface {
Validate() error
}
// IndexBuilder function type for building custom indices
type IndexBuilder[T any] func(allItems map[int]*T) any
// BaseStore provides generic storage with index management and constraints
type BaseStore[T any] struct {
items map[int]*T
maxID int
mu sync.RWMutex
itemType reflect.Type
indices map[string]any
indexBuilders map[string]IndexBuilder[T]
schema *SchemaInfo
uniqueIndices map[string]map[any]int // field -> value -> id
}
// NewBaseStore creates a new base store for type T with schema parsing
func NewBaseStore[T any]() *BaseStore[T] {
var zero T
schema := ParseSchema[T]()
store := &BaseStore[T]{
items: make(map[int]*T),
maxID: 0,
itemType: reflect.TypeOf(zero),
indices: make(map[string]any),
indexBuilders: make(map[string]IndexBuilder[T]),
schema: schema,
uniqueIndices: make(map[string]map[any]int),
}
// Initialize unique indices
for fieldName, constraints := range schema.Constraints {
for _, constraint := range constraints {
if constraint.Type == ConstraintUnique {
store.uniqueIndices[fieldName] = make(map[any]int)
}
}
}
// Auto-register indices for indexed fields
store.registerSchemaIndices()
return store
}
func (bs *BaseStore[T]) registerSchemaIndices() {
for fieldName, indexName := range bs.schema.Indices {
bs.RegisterIndex(indexName, BuildFieldLookupIndex[T](fieldName))
}
}
// ValidateConstraints checks all constraints for an item
func (bs *BaseStore[T]) ValidateConstraints(id int, item *T) error {
itemValue := reflect.ValueOf(item).Elem()
for fieldName, constraints := range bs.schema.Constraints {
fieldValue := itemValue.FieldByName(fieldName)
if !fieldValue.IsValid() {
continue
}
for _, constraint := range constraints {
switch constraint.Type {
case ConstraintRequired:
if isZeroValue(fieldValue) {
return fmt.Errorf("field %s is required", fieldName)
}
case ConstraintUnique:
value := fieldValue.Interface()
if existingID, exists := bs.uniqueIndices[fieldName][value]; exists && existingID != id {
return fmt.Errorf("field %s value %v already exists", fieldName, value)
}
}
}
}
return nil
}
func isZeroValue(v reflect.Value) bool {
return v.Interface() == reflect.Zero(v.Type()).Interface()
}
func (bs *BaseStore[T]) updateUniqueIndices(id int, item *T, add bool) {
itemValue := reflect.ValueOf(item).Elem()
for fieldName := range bs.uniqueIndices {
fieldValue := itemValue.FieldByName(fieldName)
if !fieldValue.IsValid() {
continue
}
value := fieldValue.Interface()
if add {
bs.uniqueIndices[fieldName][value] = id
} else {
delete(bs.uniqueIndices[fieldName], value)
}
}
}
// RegisterIndex registers an index builder function
func (bs *BaseStore[T]) RegisterIndex(name string, builder IndexBuilder[T]) {
bs.mu.Lock()
defer bs.mu.Unlock()
bs.indexBuilders[name] = builder
}
// GetIndex retrieves a named index
func (bs *BaseStore[T]) GetIndex(name string) (any, bool) {
bs.mu.RLock()
defer bs.mu.RUnlock()
index, exists := bs.indices[name]
return index, exists
}
// RebuildIndices rebuilds all registered indices
func (bs *BaseStore[T]) RebuildIndices() {
bs.mu.Lock()
defer bs.mu.Unlock()
bs.rebuildIndicesUnsafe()
}
func (bs *BaseStore[T]) rebuildIndicesUnsafe() {
allItems := make(map[int]*T, len(bs.items))
maps.Copy(allItems, bs.items)
for name, builder := range bs.indexBuilders {
bs.indices[name] = builder(allItems)
}
}
// AddWithValidation adds item with constraint validation and index rebuild
func (bs *BaseStore[T]) AddWithValidation(id int, item *T) error {
bs.mu.Lock()
defer bs.mu.Unlock()
// Validate constraints
if err := bs.ValidateConstraints(id, item); err != nil {
return err
}
// Custom validation
if validatable, ok := any(item).(Validatable); ok {
if err := validatable.Validate(); err != nil {
return err
}
}
// Update unique indices
bs.updateUniqueIndices(id, item, true)
bs.items[id] = item
if id > bs.maxID {
bs.maxID = id
}
bs.rebuildIndicesUnsafe()
return nil
}
// AddWithRebuild adds item with validation and index rebuild
func (bs *BaseStore[T]) AddWithRebuild(id int, item *T) error {
return bs.AddWithValidation(id, item)
}
// RemoveWithValidation removes item and updates constraints
func (bs *BaseStore[T]) RemoveWithValidation(id int) {
bs.mu.Lock()
defer bs.mu.Unlock()
if item, exists := bs.items[id]; exists {
bs.updateUniqueIndices(id, item, false)
}
delete(bs.items, id)
bs.rebuildIndicesUnsafe()
}
// RemoveWithRebuild removes item and rebuilds indices
func (bs *BaseStore[T]) RemoveWithRebuild(id int) {
bs.RemoveWithValidation(id)
}
// UpdateWithValidation updates item with validation and index rebuild
func (bs *BaseStore[T]) UpdateWithValidation(id int, item *T) error {
return bs.AddWithValidation(id, item)
}
// UpdateWithRebuild updates item with validation and index rebuild
func (bs *BaseStore[T]) UpdateWithRebuild(id int, item *T) error {
return bs.AddWithValidation(id, item)
}
// Find retrieves an item by ID
func (bs *BaseStore[T]) Find(id int) (*T, bool) {
bs.mu.RLock()
defer bs.mu.RUnlock()
item, exists := bs.items[id]
return item, exists
}
// AllSorted returns all items using named sorted index
func (bs *BaseStore[T]) AllSorted(indexName string) []*T {
bs.mu.RLock()
defer bs.mu.RUnlock()
if index, exists := bs.indices[indexName]; exists {
if sortedIDs, ok := index.([]int); ok {
result := make([]*T, 0, len(sortedIDs))
for _, id := range sortedIDs {
if item, exists := bs.items[id]; exists {
result = append(result, item)
}
}
return result
}
}
// Fallback: return all items by ID order
ids := make([]int, 0, len(bs.items))
for id := range bs.items {
ids = append(ids, id)
}
sort.Ints(ids)
result := make([]*T, 0, len(ids))
for _, id := range ids {
result = append(result, bs.items[id])
}
return result
}
// LookupByIndex finds single item using string lookup index
func (bs *BaseStore[T]) LookupByIndex(indexName, key string) (*T, bool) {
bs.mu.RLock()
defer bs.mu.RUnlock()
if index, exists := bs.indices[indexName]; exists {
if lookupMap, ok := index.(map[string]int); ok {
if id, found := lookupMap[key]; found {
if item, exists := bs.items[id]; exists {
return item, true
}
}
}
}
return nil, false
}
// GroupByIndex returns items grouped by key
func (bs *BaseStore[T]) GroupByIndex(indexName string, key any) []*T {
bs.mu.RLock()
defer bs.mu.RUnlock()
if index, exists := bs.indices[indexName]; exists {
switch groupMap := index.(type) {
case map[int][]int:
if intKey, ok := key.(int); ok {
if ids, found := groupMap[intKey]; found {
result := make([]*T, 0, len(ids))
for _, id := range ids {
if item, exists := bs.items[id]; exists {
result = append(result, item)
}
}
return result
}
}
case map[string][]int:
if strKey, ok := key.(string); ok {
if ids, found := groupMap[strKey]; found {
result := make([]*T, 0, len(ids))
for _, id := range ids {
if item, exists := bs.items[id]; exists {
result = append(result, item)
}
}
return result
}
}
}
}
return []*T{}
}
// FilterByIndex returns items matching filter criteria
func (bs *BaseStore[T]) FilterByIndex(indexName string, filterFunc func(*T) bool) []*T {
bs.mu.RLock()
defer bs.mu.RUnlock()
var sourceIDs []int
if index, exists := bs.indices[indexName]; exists {
if sortedIDs, ok := index.([]int); ok {
sourceIDs = sortedIDs
}
}
if sourceIDs == nil {
for id := range bs.items {
sourceIDs = append(sourceIDs, id)
}
sort.Ints(sourceIDs)
}
var result []*T
for _, id := range sourceIDs {
if item, exists := bs.items[id]; exists && filterFunc(item) {
result = append(result, item)
}
}
return result
}
// GetNextID returns the next available ID atomically
func (bs *BaseStore[T]) GetNextID() int {
bs.mu.Lock()
defer bs.mu.Unlock()
bs.maxID++
return bs.maxID
}
// GetByID retrieves an item by ID
func (bs *BaseStore[T]) GetByID(id int) (*T, bool) {
return bs.Find(id)
}
// Add adds an item to the store (no validation)
func (bs *BaseStore[T]) Add(id int, item *T) {
bs.mu.Lock()
defer bs.mu.Unlock()
bs.items[id] = item
if id > bs.maxID {
bs.maxID = id
}
}
// Remove removes an item from the store (no validation)
func (bs *BaseStore[T]) Remove(id int) {
bs.mu.Lock()
defer bs.mu.Unlock()
delete(bs.items, id)
}
// GetAll returns all items
func (bs *BaseStore[T]) GetAll() map[int]*T {
bs.mu.RLock()
defer bs.mu.RUnlock()
result := make(map[int]*T, len(bs.items))
maps.Copy(result, bs.items)
return result
}
// Clear removes all items
func (bs *BaseStore[T]) Clear() {
bs.mu.Lock()
defer bs.mu.Unlock()
bs.items = make(map[int]*T)
bs.maxID = 0
// Clear unique indices
for fieldName := range bs.uniqueIndices {
bs.uniqueIndices[fieldName] = make(map[any]int)
}
bs.rebuildIndicesUnsafe()
}
// Index Builder Functions
// BuildFieldLookupIndex creates index for any field by name
func BuildFieldLookupIndex[T any](fieldName string) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[string]int)
for id, item := range allItems {
itemValue := reflect.ValueOf(item).Elem()
fieldValue := itemValue.FieldByName(fieldName)
if !fieldValue.IsValid() {
continue
}
key := fmt.Sprintf("%v", fieldValue.Interface())
index[key] = id
}
return index
}
}
// BuildStringLookupIndex creates string-to-ID mapping
func BuildStringLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[string]int)
for id, item := range allItems {
key := keyFunc(item)
index[key] = id
}
return index
}
}
// BuildCaseInsensitiveLookupIndex creates lowercase string-to-ID mapping
func BuildCaseInsensitiveLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[string]int)
for id, item := range allItems {
key := strings.ToLower(keyFunc(item))
index[key] = id
}
return index
}
}
// BuildIntGroupIndex creates int-to-[]ID mapping
func BuildIntGroupIndex[T any](keyFunc func(*T) int) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[int][]int)
for id, item := range allItems {
key := keyFunc(item)
index[key] = append(index[key], id)
}
// Sort each group by ID
for key := range index {
sort.Ints(index[key])
}
return index
}
}
// BuildStringGroupIndex creates string-to-[]ID mapping
func BuildStringGroupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[string][]int)
for id, item := range allItems {
key := keyFunc(item)
index[key] = append(index[key], id)
}
// Sort each group by ID
for key := range index {
sort.Ints(index[key])
}
return index
}
}
// BuildSortedListIndex creates sorted []ID list
func BuildSortedListIndex[T any](sortFunc func(*T, *T) bool) IndexBuilder[T] {
return func(allItems map[int]*T) any {
ids := make([]int, 0, len(allItems))
for id := range allItems {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool {
return sortFunc(allItems[ids[i]], allItems[ids[j]])
})
return ids
}
}
// BuildFilteredIntGroupIndex creates int-to-[]ID mapping for items passing filter
func BuildFilteredIntGroupIndex[T any](filterFunc func(*T) bool, keyFunc func(*T) int) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[int][]int)
for id, item := range allItems {
if filterFunc(item) {
key := keyFunc(item)
index[key] = append(index[key], id)
}
}
// Sort each group by ID
for key := range index {
sort.Ints(index[key])
}
return index
}
}
// BuildFilteredStringGroupIndex creates string-to-[]ID mapping for items passing filter
func BuildFilteredStringGroupIndex[T any](filterFunc func(*T) bool, keyFunc func(*T) string) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[string][]int)
for id, item := range allItems {
if filterFunc(item) {
key := keyFunc(item)
index[key] = append(index[key], id)
}
}
// Sort each group by ID
for key := range index {
sort.Ints(index[key])
}
return index
}
}
// NewSingleton creates singleton store pattern with sync.Once
func NewSingleton[S any](initFunc func() *S) func() *S {
var store *S
var once sync.Once
return func() *S {
once.Do(func() {
store = initFunc()
})
return store
}
}