├── .gitignore ├── LICENSE ├── README.md ├── store.go └── store_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ian Byrd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Store 2 | >Store is a dead simple configuration manager for Go applications. 3 | 4 | [![GoDoc](https://godoc.org/github.com/tucnak/store?status.svg)](https://godoc.org/github.com/tucnak/store) 5 | 6 | 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% 7 | on Windows. 8 | 9 | Look, when I say it's dead simple, I actually mean it: 10 | ```go 11 | package main 12 | 13 | import ( 14 | "log" 15 | "time" 16 | 17 | "github.com/tucnak/store" 18 | ) 19 | 20 | func init() { 21 | // You must init store with some truly unique path first! 22 | store.Init("cats-n-dogs/project-hotel") 23 | } 24 | 25 | type Cat struct { 26 | Name string `toml:"naym"` 27 | Clever bool `toml:"ayy"` 28 | } 29 | 30 | type Hotel struct { 31 | Name string 32 | Cats []Cat `toml:"guests"` 33 | 34 | Opens *time.Time 35 | Closes *time.Time 36 | } 37 | 38 | func main() { 39 | var hotel Hotel 40 | 41 | if err := store.Load("hotel.toml", &hotel); err != nil { 42 | log.Println("failed to load the cat hotel:", err) 43 | return 44 | } 45 | 46 | // ... 47 | 48 | if err := store.Save("hotel.toml", &hotel); err != nil { 49 | log.Println("failed to save the cat hotel:", err) 50 | return 51 | } 52 | } 53 | ``` 54 | 55 | 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: 56 | ```go 57 | store.Register("ini", ini.Marshal, ini.Unmarshal) 58 | 59 | err := store.Load("configuration.ini", &object) 60 | // ... 61 | ``` 62 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | // Package store is a dead simple configuration manager for Go applications. 2 | package store 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "runtime" 13 | 14 | "github.com/BurntSushi/toml" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | // MarshalFunc is any marshaler. 19 | type MarshalFunc func(v interface{}) ([]byte, error) 20 | 21 | // UnmarshalFunc is any unmarshaler. 22 | type UnmarshalFunc func(data []byte, v interface{}) error 23 | 24 | var ( 25 | applicationName = "" 26 | formats = map[string]format{} 27 | ) 28 | 29 | type format struct { 30 | m MarshalFunc 31 | um UnmarshalFunc 32 | } 33 | 34 | func init() { 35 | formats["json"] = format{m: json.Marshal, um: json.Unmarshal} 36 | formats["yaml"] = format{m: yaml.Marshal, um: yaml.Unmarshal} 37 | formats["yml"] = format{m: yaml.Marshal, um: yaml.Unmarshal} 38 | 39 | formats["toml"] = format{ 40 | m: func(v interface{}) ([]byte, error) { 41 | b := bytes.Buffer{} 42 | err := toml.NewEncoder(&b).Encode(v) 43 | return b.Bytes(), err 44 | }, 45 | um: toml.Unmarshal, 46 | } 47 | } 48 | 49 | // Init sets up a unique application name that will be used for name of the 50 | // configuration directory on the file system. By default, Store puts all the 51 | // config data to to $XDG_CONFIG_HOME or $HOME on Linux systems 52 | // and to %APPDATA% on Windows. 53 | // 54 | // Beware: Store will panic on any sensitive calls unless you run Init inb4. 55 | func Init(application string) { 56 | applicationName = application 57 | } 58 | 59 | // Register is the way you register configuration formats, by mapping some 60 | // file name extension to corresponding marshal and unmarshal functions. 61 | // Once registered, the format given would be compatible with Load and Save. 62 | func Register(extension string, m MarshalFunc, um UnmarshalFunc) { 63 | formats[extension] = format{m, um} 64 | } 65 | 66 | // Load reads a configuration from `path` and puts it into `v` pointer. Store 67 | // supports either JSON, TOML or YAML and will deduce the file format out of 68 | // the filename (.json/.toml/.yaml). For other formats of custom extensions 69 | // please you LoadWith. 70 | // 71 | // Path is a full filename, including the file extension, e.g. "foobar.json". 72 | // If `path` doesn't exist, Load will create one and emptify `v` pointer by 73 | // replacing it with a newly created object, derived from type of `v`. 74 | // 75 | // Load panics on unknown configuration formats. 76 | func Load(path string, v interface{}) error { 77 | if applicationName == "" { 78 | panic("store: application name not defined") 79 | } 80 | 81 | if format, ok := formats[extension(path)]; ok { 82 | return LoadWith(path, v, format.um) 83 | } 84 | 85 | panic("store: unknown configuration format") 86 | } 87 | 88 | // Save puts a configuration from `v` pointer into a file `path`. Store 89 | // supports either JSON, TOML or YAML and will deduce the file format out of 90 | // the filename (.json/.toml/.yaml). For other formats of custom extensions 91 | // please you LoadWith. 92 | // 93 | // Path is a full filename, including the file extension, e.g. "foobar.json". 94 | // 95 | // Save panics on unknown configuration formats. 96 | func Save(path string, v interface{}) error { 97 | if applicationName == "" { 98 | panic("store: application name not defined") 99 | } 100 | 101 | if format, ok := formats[extension(path)]; ok { 102 | return SaveWith(path, v, format.m) 103 | } 104 | 105 | panic("store: unknown configuration format") 106 | } 107 | 108 | // LoadWith loads the configuration using any unmarshaler at all. 109 | func LoadWith(path string, v interface{}, um UnmarshalFunc) error { 110 | if applicationName == "" { 111 | panic("store: application name not defined") 112 | } 113 | 114 | globalPath := buildPlatformPath(path) 115 | 116 | data, err := ioutil.ReadFile(globalPath) 117 | 118 | if err != nil { 119 | // There is a chance that file we are looking for 120 | // just doesn't exist. In this case we are supposed 121 | // to create an empty configuration file, based on v. 122 | empty := reflect.New(reflect.TypeOf(v)) 123 | if innerErr := Save(path, &empty); innerErr != nil { 124 | // Smth going on with the file system... returning error. 125 | return err 126 | } 127 | 128 | v = empty 129 | 130 | return nil 131 | } 132 | 133 | if err := um(data, v); err != nil { 134 | return fmt.Errorf("store: failed to unmarshal %s: %v", path, err) 135 | } 136 | 137 | return nil 138 | } 139 | 140 | // SaveWith saves the configuration using any marshaler at all. 141 | func SaveWith(path string, v interface{}, m MarshalFunc) error { 142 | if applicationName == "" { 143 | panic("store: application name not defined") 144 | } 145 | 146 | var b bytes.Buffer 147 | 148 | if data, err := m(v); err == nil { 149 | b.Write(data) 150 | } else { 151 | return fmt.Errorf("store: failed to marshal %s: %v", path, err) 152 | } 153 | 154 | b.WriteRune('\n') 155 | 156 | globalPath := buildPlatformPath(path) 157 | if err := os.MkdirAll(filepath.Dir(globalPath), os.ModePerm); err != nil { 158 | return err 159 | } 160 | 161 | if err := ioutil.WriteFile(globalPath, b.Bytes(), os.ModePerm); err != nil { 162 | return err 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func extension(path string) string { 169 | for i := len(path) - 1; i >= 0; i-- { 170 | if path[i] == '.' { 171 | return path[i+1:] 172 | } 173 | } 174 | 175 | return "" 176 | } 177 | 178 | // buildPlatformPath builds a platform-dependent path for relative path given. 179 | func buildPlatformPath(path string) string { 180 | if runtime.GOOS == "windows" { 181 | return fmt.Sprintf("%s\\%s\\%s", os.Getenv("APPDATA"), 182 | applicationName, 183 | path) 184 | } 185 | 186 | var unixConfigDir string 187 | if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { 188 | unixConfigDir = xdg 189 | } else { 190 | unixConfigDir = os.Getenv("HOME") + "/.config" 191 | } 192 | 193 | return fmt.Sprintf("%s/%s/%s", unixConfigDir, 194 | applicationName, 195 | path) 196 | } 197 | 198 | // SetApplicationName is DEPRECATED (use Init instead). 199 | func SetApplicationName(handle string) { 200 | applicationName = handle 201 | } 202 | -------------------------------------------------------------------------------- /store_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | type Cat struct { 9 | Name string 10 | Big bool 11 | } 12 | 13 | type Settings struct { 14 | Age int 15 | Cats []Cat 16 | RandomString string 17 | } 18 | 19 | func equal(a, b Settings) bool { 20 | if a.Age != b.Age { 21 | return false 22 | } 23 | 24 | if a.RandomString != b.RandomString { 25 | return false 26 | } 27 | 28 | if len(a.Cats) != len(b.Cats) { 29 | return false 30 | } 31 | 32 | for i, cat := range a.Cats { 33 | if cat != b.Cats[i] { 34 | return false 35 | } 36 | } 37 | 38 | return true 39 | } 40 | 41 | func TestSaveLoad(t *testing.T) { 42 | Init("store_test") 43 | 44 | settings := Settings{ 45 | Age: 42, 46 | Cats: []Cat{ 47 | Cat{"Rudolph", true}, 48 | Cat{"Patrick", false}, 49 | Cat{"Jeremy", true}, 50 | }, 51 | RandomString: "gophers are gonna conquer the world", 52 | } 53 | 54 | settingsFile := "path/to/preferences.toml" 55 | 56 | err := Save(settingsFile, &settings) 57 | if err != nil { 58 | t.Fatalf("failed to save preferences: %s\n", err) 59 | return 60 | } 61 | 62 | defer os.Remove(buildPlatformPath(settingsFile)) 63 | 64 | var newSettings Settings 65 | 66 | err = Load(settingsFile, &newSettings) 67 | if err != nil { 68 | t.Fatalf("failed to load preferences: %s\n", err) 69 | return 70 | } 71 | 72 | if !equal(settings, newSettings) { 73 | t.Fatalf("broken") 74 | } 75 | } 76 | --------------------------------------------------------------------------------