204 lines
3.8 KiB
Go

package ocean
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"sync"
)
type Collection[T any] interface {
All() <-chan *T
Clear()
Delete(key string)
Exists(key string) bool
Filter(func(*T) bool) <-chan *T
Get(key string) (value *T, err error)
Set(key string, value *T)
}
// collection is a hash map of homogeneous data.
type collection[T any] struct {
data sync.Map
name string
directory string
}
// New creates a new collection with the given name.
func New[T any](directories ...string) (*collection[T], error) {
name := reflect.TypeOf((*T)(nil)).Elem().Name()
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
directories = append([]string{home, ".ocean"}, directories...)
directories = append(directories, name)
directory := filepath.Join(directories...)
err = os.MkdirAll(directory, 0700)
if err != nil {
return nil, err
}
c := &collection[T]{
name: directories[len(directories)-1],
directory: directory,
}
return c, c.loadFromDisk()
}
// All returns a channel of all objects in the collection.
func (c *collection[T]) All() <-chan *T {
channel := make(chan *T)
go func() {
c.data.Range(func(key, value any) bool {
channel <- value.(*T)
return true
})
close(channel)
}()
return channel
}
// Filter returns a channel of all objects that pass the given filter function.
func (c *collection[T]) Filter(filter func(*T) bool) <-chan *T {
channel := make(chan *T)
go func() {
c.data.Range(func(key, value any) bool {
if filter(value.(*T)) {
channel <- value.(*T)
}
return true
})
close(channel)
}()
return channel
}
// Get returns the value for the given key.
func (c *collection[T]) Get(key string) (*T, error) {
value, exists := c.data.Load(key)
if !exists {
return nil, &KeyNotFoundError{Key: key}
}
return value.(*T), nil
}
// Set sets the value for the given key.
func (c *collection[T]) Set(key string, value *T) {
c.data.Store(key, value)
err := c.writeFileToDisk(key, value)
if err != nil {
panic(err)
}
}
// Delete deletes a key from the collection.
func (c *collection[T]) Delete(key string) {
if !c.Exists(key) {
return
}
c.data.Delete(key)
os.Remove(c.keyFile(key))
}
// Exists returns whether or not the key exists.
func (c *collection[T]) Exists(key string) bool {
_, exists := c.data.Load(key)
return exists
}
// Clear deletes all objects from the collection.
func (c *collection[T]) Clear() {
c.data.Range(func(key, value any) bool {
c.Delete(key.(string))
return true
})
}
// keyFile returns the file path for the given key.
func (c *collection[T]) keyFile(key string) string {
return filepath.Join(c.directory, key)
}
// loadFromDisk loads the collection data from the disk.
func (c *collection[T]) loadFromDisk() error {
file, err := os.Open(c.directory)
if err != nil {
return err
}
files, err := file.Readdirnames(0)
for _, key := range files {
fileError := c.loadFileFromDisk(key)
if fileError != nil {
return fileError
}
}
if err != nil {
return err
}
return file.Close()
}
// loadFileFromDisk loads a single file from the disk.
func (c *collection[T]) loadFileFromDisk(key string) error {
file, err := os.Open(filepath.Join(c.directory, key))
if err != nil {
return err
}
value := new(T)
decoder := json.NewDecoder(file)
err = decoder.Decode(value)
if err != nil {
file.Close()
return err
}
c.data.Store(key, value)
return file.Close()
}
// writeFileToDisk writes the value for the key to disk as a JSON file.
func (c *collection[T]) writeFileToDisk(key string, value *T) error {
fileName := c.keyFile(key)
file, err := os.Create(fileName)
if err != nil {
return err
}
encoder := json.NewEncoder(file)
err = encoder.Encode(value)
if err != nil {
file.Close()
return err
}
return file.Close()
}