diff --git a/Collection.go b/Collection.go index 6c29107..f349742 100644 --- a/Collection.go +++ b/Collection.go @@ -4,15 +4,18 @@ import ( "encoding/json" "os" "path" + "path/filepath" + "strings" "sync" ) type Collection[T any] interface { - All() <-chan T + All() <-chan *T + Clear() Delete(key string) Exists(key string) bool - Get(key string) (value T, ok bool) - Set(key string, value T) + Get(key string) (value *T, ok bool) + Set(key string, value *T) } // collection is a hash map of homogeneous data. @@ -42,54 +45,16 @@ func NewCollection[T any](namespace string, name string) (*collection[T], error) directory: directory, } - return c, err -} - -// Get returns the value for the given key. -func (c *collection[T]) Get(key string) (T, bool) { - value, exists := c.data.Load(key) - return value.(T), exists -} - -// Set sets the value for the given key. -func (c *collection[T]) Set(key string, value T) { - c.data.Store(key, value) - - fileName := path.Join(c.directory, key+".json") - file, err := os.Create(fileName) - - if err != nil { - panic(err) - } - - encoder := json.NewEncoder(file) - encoder.Encode(value) - - err = file.Close() - - if err != nil { - panic(err) - } -} - -// Delete deletes a key from the collection. -func (c *collection[T]) Delete(key string) { - c.data.Delete(key) -} - -// Exists returns whether or not the key exists. -func (c *collection[T]) Exists(key string) bool { - _, exists := c.data.Load(key) - return exists + 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) +func (c *collection[T]) All() <-chan *T { + channel := make(chan *T) go func() { c.data.Range(func(key, value any) bool { - channel <- value.(T) + channel <- value.(*T) return true }) @@ -98,3 +63,109 @@ func (c *collection[T]) All() <-chan T { return channel } + +// Get returns the value for the given key. +func (c *collection[T]) Get(key string) (*T, bool) { + value, exists := c.data.Load(key) + return value.(*T), exists +} + +// Set sets the value for the given key. +func (c *collection[T]) Set(key string, value *T) { + c.data.Store(key, value) + c.writeFileToDisk(key, value) +} + +// Delete deletes a key from the collection. +func (c *collection[T]) Delete(key string) { + 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+".json") +} + +// 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 _, name := range files { + fileError := c.loadFileFromDisk(name) + + 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(name string) error { + file, err := os.Open(filepath.Join(c.directory, name)) + + if err != nil { + return err + } + + value := new(T) + decoder := json.NewDecoder(file) + err = decoder.Decode(value) + + if err != nil { + return err + } + + key := strings.TrimSuffix(name, filepath.Ext(name)) + 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) { + fileName := c.keyFile(key) + file, err := os.Create(fileName) + + if err != nil { + panic(err) + } + + encoder := json.NewEncoder(file) + err = encoder.Encode(value) + + if err != nil { + panic(err) + } + + err = file.Close() + + if err != nil { + panic(err) + } +} diff --git a/Collection_test.go b/Collection_test.go index 2d02436..64a0aa7 100644 --- a/Collection_test.go +++ b/Collection_test.go @@ -7,45 +7,36 @@ import ( "git.akyoto.dev/go/ocean" ) -func TestCollectionGet(t *testing.T) { - users, err := ocean.NewCollection[string]("test", "User") - assert.Nil(t, err) +type User struct { + Name string `json:"name"` +} - users.Set("1", "abc") +func TestCollectionGet(t *testing.T) { + users, err := ocean.NewCollection[User]("test", "User") + assert.Nil(t, err) + defer users.Clear() + + users.Set("1", &User{Name: "User 1"}) user, exists := users.Get("1") assert.True(t, exists) assert.NotNil(t, user) } -func TestCollectionAll(t *testing.T) { - users, err := ocean.NewCollection[string]("test", "User") - assert.Nil(t, err) - - users.Set("1", "abc") - users.Set("2", "def") - count := 0 - - for range users.All() { - count++ - } - - assert.Equal(t, count, 2) -} - func TestInteraction(t *testing.T) { - users, err := ocean.NewCollection[string]("test", "User") + users, err := ocean.NewCollection[User]("test", "User") assert.Nil(t, err) + defer users.Clear() assert.True(t, !users.Exists("1")) assert.True(t, !users.Exists("2")) - users.Set("1", "abc") + users.Set("1", &User{Name: "User 1"}) assert.True(t, users.Exists("1")) assert.True(t, !users.Exists("2")) - users.Set("2", "def") + users.Set("2", &User{Name: "User 1"}) assert.True(t, users.Exists("1")) assert.True(t, users.Exists("2")) @@ -60,3 +51,19 @@ func TestInteraction(t *testing.T) { assert.True(t, !users.Exists("1")) assert.True(t, !users.Exists("2")) } + +func TestCollectionAll(t *testing.T) { + users, err := ocean.NewCollection[User]("test", "User") + assert.Nil(t, err) + defer users.Clear() + + users.Set("1", &User{Name: "User 1"}) + users.Set("2", &User{Name: "User 2"}) + count := 0 + + for range users.All() { + count++ + } + + assert.Equal(t, count, 2) +} diff --git a/README.md b/README.md index 1b08949..e9c7448 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,7 @@ In-memory key value store that saves your data to plain old JSON files. If you like, you can operate on your entire data with classic UNIX tools. -Writing to disk is async, so the performance of your code is only limited by your CPU and RAM speed. +```go +users := ocean.NewCollection("namespace", "User") +users.Set("1", &User{Name: "User 1"}) +```