Skip to content

Commit

Permalink
{Save/Load}With, Register and Init introduced.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ian Byrd committed Jul 7, 2016
1 parent 42d3cf7 commit 84b795b
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 75 deletions.
56 changes: 30 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
[![GoDoc](https://godoc.org/github.com/tucnak/store?status.svg)](https://godoc.org/github.com/tucnak/store)

I didn't like existing configuration management solutions like [globalconf](https://github.com/rakyll/globalconf), [tachyon](https://github.com/vektra/tachyon) or [viper](https://github.com/spf13/viper). First two just don't feel right and viper, imo, a little overcomplicated—definitely too much for small projects.
I didn't like existing configuration management solutions like [globalconf](https://github.com/rakyll/globalconf), [tachyon](https://github.com/vektra/tachyon) or [viper](https://github.com/spf13/viper). First two just don't feel right and viper, imo, a little overcomplicated—definitely offering too much for small things. Store supports either JSON, TOML or YAML out-of-the-box and lets you register practically any other configuration format. It persists all of your configurations in either $XDG_CONFIG_HOME or $HOME on Linux and in %APPDATA%
on Windows.

Store currenty supports JSON and TOML and I am not planning to add support for other file formats soon.

Here is a hot example of Store in the wild:
Look, when I say it's dead simple, I actually mean it:
```go
package main

Expand All @@ -17,41 +16,46 @@ import (
"os"
)

type Cat struct {
Name string
Big bool
func init() {
// You must init store with some truly unique path first!
store.Init("cats-n-dogs/project-hotel")
}

type Settings struct {
Age int
Cats []Cat
RandomString string
type Cat struct {
Name string `toml:"naym"`
Clever bool `toml:"ayy"`
}

func init() {
// By default, Store puts all your config data to %APPDATA%/<appname>
// on Windows and to $XDG_CONFIG_HOME or $HOME on *unix systems.
//
// Warning: Store would panic on any sensitive calls if it's not set.
store.SetApplicationName("joecockerfanclub")
type Hotel struct {
Name string
Cats []Cat `toml:"guests"`

Opens *time.Time
Closes *time.Time
}

func main() {
var settings Settings
err := store.Load("preferences.toml", &settings)
var hotel Hotel
err := store.Load("hotel.toml", &settings)
if err != nil {
log.Printf("failed to load preferences: %s\n", err)
os.Exit(1)
log.Println("failed to load the cat hotel:", err)
return
}

// Some work...
// ...

err := store.Save("preferences.toml", &settings)
err := store.Save("hotel.toml", &settings)
if err != nil {
log.Printf("failed to save preferences: %s\n", err)
os.Exit(1)
log.Println("failed to save the cat hotel:", err)
return
}
}
```

Read [godoc](https://godoc.org/github.com/tucnak/store) to get more familiar.
Store supports any other formats via the handy registration system: register the format once and you'd be able to Load and Save files in it afterwards:
```go
store.Register("ini", ini.Marshal, ini.Unmarshal)

err := store.Load("configuration.ini", &object)
// ...
```
155 changes: 110 additions & 45 deletions store.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,112 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/BurntSushi/toml"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"

"github.com/BurntSushi/toml"
"github.com/go-yaml/yaml"
)

var applicationName string
// MarshalFunc is any marshaler.
type MarshalFunc func(v interface{}) ([]byte, error)

// SetApplicationName defines a unique application handle for file system.
//
// By default, Store puts all your config data to %APPDATA%/<appname> on Windows
// and to $XDG_CONFIG_HOME or $HOME on *unix systems.
//
// Warning: Store would panic on any sensitive calls if it's not set.
func SetApplicationName(handle string) {
applicationName = handle
// UnmarshalFunc is any unmarshaler.
type UnmarshalFunc func(data []byte, v interface{}) error

var (
applicationName = ""
formats = map[string]format{}
)

type format struct {
m MarshalFunc
um UnmarshalFunc
}

// Load reads a configuration from `path` and puts it into `v` pointer.
func init() {
formats["json"] = format{m: json.Marshal, um: json.Unmarshal}
formats["yaml"] = format{m: yaml.Marshal, um: yaml.Unmarshal}
formats["yml"] = format{m: yaml.Marshal, um: yaml.Unmarshal}

formats["toml"] = format{
m: func(v interface{}) ([]byte, error) {
b := bytes.Buffer{}
err := toml.NewEncoder(&b).Encode(v)
return b.Bytes(), err
},
um: toml.Unmarshal,
}
}

// Init sets up a unique application name that will be used for name of the
// configuration directory on the file system. By default, Store puts all the
// config data to to $XDG_CONFIG_HOME or $HOME on Linux systems
// and to %APPDATA% on Windows.
//
// Path is a full filename, with extension. Since Store currently support
// TOML and JSON only, passing others would result in a corresponding error.
// Beware: Store will panic on any sensitive calls unless you run Init inb4.
func Init(application string) {
applicationName = application
}

// Register is the way you register configuration formats, by mapping some
// file name extension to corresponding marshal and unmarshal functions.
// Once registered, the format given would be compatible with Load and Save.
func Register(extension string, m MarshalFunc, um UnmarshalFunc) {
formats[extension] = format{m, um}
}

// Load reads a configuration from `path` and puts it into `v` pointer. Store
// supports either JSON, TOML or YAML and will deduce the file format out of
// the filename (.json/.toml/.yaml). For other formats of custom extensions
// please you LoadWith.
//
// Path is a full filename, including the file extension, e.g. "foobar.json".
// If `path` doesn't exist, Load will create one and emptify `v` pointer by
// replacing it with a newly created object, derived from type of `v`.
//
// Load panics on unknown configuration formats.
func Load(path string, v interface{}) error {
if applicationName == "" {
panic("store: application name not defined")
}

if format, ok := formats[extension(path)]; ok {
return LoadWith(path, v, format.um)
}

panic("store: unknown configuration format")
}

// Save puts a configuration from `v` pointer into a file `path`. Store
// supports either JSON, TOML or YAML and will deduce the file format out of
// the filename (.json/.toml/.yaml). For other formats of custom extensions
// please you LoadWith.
//
// Path is a full filename, including the file extension, e.g. "foobar.json".
//
// Save panics on unknown configuration formats.
func Save(path string, v interface{}) error {
if applicationName == "" {
panic("store: application name not defined")
}

if format, ok := formats[extension(path)]; ok {
return SaveWith(path, v, format.m)
}

panic("store: unknown configuration format")
}

// LoadWith loads the configuration using any unmarshaler at all.
func LoadWith(path string, v interface{}, um UnmarshalFunc) error {
if applicationName == "" {
panic("store: application name not defined")
}

globalPath := buildPlatformPath(path)

data, err := ioutil.ReadFile(globalPath)
Expand All @@ -48,7 +121,7 @@ func Load(path string, v interface{}) error {
// to create an empty configuration file, based on v.
empty := reflect.New(reflect.TypeOf(v))
if innerErr := Save(path, &empty); innerErr != nil {
// Must be smth with file system... returning error from read.
// Smth going on with the file system... returning error.
return err
}

Expand All @@ -57,48 +130,25 @@ func Load(path string, v interface{}) error {
return nil
}

contents := string(data)
if strings.HasSuffix(path, ".toml") {
if _, err := toml.Decode(contents, v); err != nil {
return err
}
} else if strings.HasSuffix(path, ".json") {
if err := json.Unmarshal(data, v); err != nil {
return err
}
} else {
return fmt.Errorf("store: unknown configuration format")
if err := um(data, v); err != nil {
return fmt.Errorf("store: failed to unmarshal %s: %v", path, err)
}

return nil
}

// Save puts a configuration from `v` pointer into a file `path`.
//
// Path is a full filename, with extension. Since Store currently support
// TOML and JSON only, passing others would result in a corresponding error.
func Save(path string, v interface{}) error {
// SaveWith saves the configuration using any marshaler at all.
func SaveWith(path string, v interface{}, m MarshalFunc) error {
if applicationName == "" {
panic("store: application name not defined")
}

var b bytes.Buffer

if strings.HasSuffix(path, ".toml") {
encoder := toml.NewEncoder(&b)
if err := encoder.Encode(v); err != nil {
return nil
}

} else if strings.HasSuffix(path, ".json") {
fileData, err := json.Marshal(v)
if err != nil {
return err
}

b.Write(fileData)
if data, err := m(v); err == nil {
b.Write(data)
} else {
return fmt.Errorf("unknown configuration format")
return fmt.Errorf("store: failed to marshal %s: %v", path, err)
}

b.WriteRune('\n')
Expand All @@ -115,6 +165,16 @@ func Save(path string, v interface{}) error {
return nil
}

func extension(path string) string {
for i := len(path) - 1; i >= 0; i-- {
if path[i] == '.' {
return path[i+1:]
}
}

return ""
}

// buildPlatformPath builds a platform-dependent path for relative path given.
func buildPlatformPath(path string) string {
if runtime.GOOS == "windows" {
Expand All @@ -134,3 +194,8 @@ func buildPlatformPath(path string) string {
applicationName,
path)
}

// SetApplicationName is DEPRECATED (use Init instead).
func SetApplicationName(handle string) {
applicationName = handle
}
6 changes: 2 additions & 4 deletions store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import (
"testing"
)

func init() {
SetApplicationName("store_test")
}

type Cat struct {
Name string
Big bool
Expand Down Expand Up @@ -43,6 +39,8 @@ func equal(a, b Settings) bool {
}

func TestSaveLoad(t *testing.T) {
Init("store_test")

settings := Settings{
Age: 42,
Cats: []Cat{
Expand Down

0 comments on commit 84b795b

Please sign in to comment.