├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── example.config ├── example_test.go ├── go.mod ├── handlers.go ├── handlers ├── big │ ├── big.go │ └── big_test.go ├── doc.go ├── doc_test.go ├── html │ └── template │ │ ├── template.go │ │ └── template_test.go ├── net │ ├── net.go │ ├── net_test.go │ └── url │ │ ├── url.go │ │ └── url_test.go └── regexp │ ├── regexp.go │ └── regexp_test.go ├── handlers_test.go ├── inflect.go ├── inflect_test.go ├── sconfig.go ├── sconfig_test.go ├── validate.go └── validate_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: arp242 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © Martin Tournoij 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 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell 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 13 | all 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 20 | from, out of or in connection with the software or the use or other dealings 21 | in the software. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `sconfig` is a simple and functional configuration file parser for Go. 2 | 3 | Import as `zgo.at/sconfig`; API docs: https://godocs.io/zgo.at/sconfig 4 | 5 | Go 1.5 and newer should work, but the test suite only runs with 1.7 and newer. 6 | 7 | What does it look like? 8 | ----------------------- 9 | 10 | A file like this: 11 | 12 | ```apache 13 | # This is a comment 14 | 15 | port 8080 # This is also a comment 16 | 17 | # Look ma, no quotes! 18 | base-url http://example.com 19 | 20 | # We'll parse these in a []*regexp.Regexp 21 | match ^foo.+ 22 | match ^b[ao]r 23 | 24 | # Two values 25 | order allow deny 26 | 27 | host # Idented lines are collapsed 28 | arp242.net # My website 29 | goatcounter.com # My other website 30 | 31 | address arp242.net 32 | ``` 33 | 34 | Can be parsed with: 35 | 36 | ```go 37 | package main 38 | 39 | import ( 40 | "fmt" 41 | "os" 42 | 43 | "zgo.at/sconfig" 44 | 45 | // Types that need imports are in handlers/pkgname 46 | _ "zgo.at/sconfig/handlers/regexp" 47 | ) 48 | 49 | type Config struct { 50 | Port int64 51 | BaseURL string 52 | Match []*regexp.Regexp 53 | Order []string 54 | Hosts []string 55 | Address string 56 | } 57 | 58 | func main() { 59 | config := Config{} 60 | err := sconfig.Parse(&config, "config", sconfig.Handlers{ 61 | // Custom handler 62 | "address": func(line []string) error { 63 | addr, err := net.LookupHost(line[0]) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | config.Address = addr[0] 69 | return nil 70 | }, 71 | }) 72 | if err != nil { 73 | fmt.Fprintf(os.Stderr, "Error parsing config: %v", err) 74 | os.Exit(1) 75 | } 76 | 77 | fmt.Printf("%#v\n", config) 78 | } 79 | ``` 80 | 81 | Will result in: 82 | 83 | example.Config{ 84 | Port: 8080, 85 | BaseURL: "http://example.com", 86 | Match: []*regexp.Regexp{[..], [..]}, 87 | Order: []string{"allow", "deny"}, 88 | Hosts: []string{"arp242.net", "goatcounter.com"}, 89 | Address: "arp242.net", 90 | } 91 | 92 | But why not... 93 | -------------- 94 | 95 | - JSON?
96 | JSON is [not intended for configuration files][json]. 97 | - YAML?
98 | I don't like the whitespace significance in config files, and [YAML can have 99 | some weird behaviour][yaml]. 100 | - XML?
101 | It's overly verbose. 102 | - INI or TOML?
103 | They're both fine, I just don't like the syntax much. Typing all those pesky 104 | `=` and `"` characters is just so much work man! 105 | - Viper?
106 | Mostly untyped, quite complex, [a lot of 107 | dependencies](https://godoc.org/github.com/spf13/viper?import-graph). 108 | 109 | Isn't "rolling your own" a bad idea? I don't think so. It's not that hard, and 110 | the syntax is simple/intuitive enough to be grokable by most people. 111 | 112 | How do I... 113 | ----------- 114 | 115 | ### Validate fields? 116 | 117 | Handlers can be chained. For example the default handler for `int64` is: 118 | 119 | RegisterType("int64", ValidateSingleValue(), handleInt64) 120 | 121 | `ValidateSingleValue()` returns a type handler that will give an error if there 122 | isn't a single value for this key; for example this is an error: 123 | 124 | foo 42 42 125 | 126 | There are several others as well. See `Validate*()` in godoc. You can add more 127 | complex validation handlers if you want, but in general I would recommend just 128 | using plain ol' `if` statements. 129 | 130 | Adding things such as tag-based validation isn't a goal at this point. I'm not 131 | at all that sure this is a common enough problem that needs solving, and there 132 | are already many other packages which do this (no need to reinvent the wheel). 133 | 134 | My personal recommendation would be [zvalidate][zvalidate], mostly because I 135 | wrote it ;-) 136 | 137 | ### Set default values? 138 | 139 | Set them before parsing: 140 | 141 | c := MyConfig{Value: "The default"} 142 | sconfig.Parse(&c, "a-file", nil) 143 | 144 | ### Override from the environment/flags/etc.? 145 | 146 | There is no direct built-in support for that, but there is `Fields()` to list 147 | all the field names. For example: 148 | 149 | c := MyConfig{Foo string} 150 | sconfig.Parse(&c, "a-file", nil) 151 | 152 | for name, val := range sconfig.Fields(&c) { 153 | if flag[name] != "" { 154 | val.SetString(flag[name]) 155 | } 156 | } 157 | 158 | ### Use `int` types? I get an error? 159 | 160 | Only `int64` and `uint64` are handled by default; this should be fine for almost 161 | all use cases of this package. If you want to add any of the other (u)int types 162 | you can do easily with your own type handler. 163 | 164 | "lol, no generics", or something, I guess. 165 | 166 | Note that the size of `int` and `uint` are platform-dependent, so adding those 167 | may not be a good idea. 168 | 169 | ### Use my own types as config fields? 170 | 171 | You have three options: 172 | 173 | - Add a type handler with `sconfig.RegisterType()`. 174 | - Make your type satisfy the `encoding.TextUnmarshaler` interface. 175 | - Add a `Handler` in `sconfig.Parse()`. 176 | 177 | ### I get a "don’t know how to set fields of the type ..." error if I try to add a new type handler 178 | 179 | Include the package name; even if the type handler is in the same package. Do: 180 | 181 | sconfig.RegisterType("[]main.RecordT", func(v []string) (interface{}, error) { .. } 182 | 183 | and not: 184 | 185 | sconfig.RegisterType("[]RecordT", func(v []string) (interface{}, error) { .. } 186 | 187 | Replace `main` with the appropriate package name. 188 | 189 | Syntax 190 | ------ 191 | 192 | The syntax of the file is very simple. 193 | 194 | ### Definitions 195 | 196 | - Whitespace: any Unicode whitespace (Zs or "Separator, Space" category). 197 | - Hash: `#` (U+0023), Backslash: `\` (U+005C), Space: a space (U+0020), NULL: U+0000 198 | - Newline: LF (U+000A) or CR+LF (U+000D, U+000A). 199 | - Line: Any set of characters ending with a Newline 200 | 201 | ### Reading the file 202 | 203 | - A file must be encoded in UTF-8. 204 | 205 | - Everything after the first Hash is considered to be a comment and will be 206 | ignored unless a Hash is immediately preceded by a Backslash. 207 | 208 | - All Whitespace is collapsed to a single Space unless a Whitespace character is 209 | preceded by a Backslash. 210 | 211 | - Any Backslash immediately preceded by a Backslash will be treated as a single 212 | Backslash. 213 | 214 | - Any Backslash immediately followed by anything other than a Hash, Whitespace, 215 | or Backslash is treated as a single Backslash. 216 | 217 | - Anything before the first Whitespace is considered the Key. 218 | 219 | - Any character except Whitespace and NULL bytes are allowed in the Key. 220 | - The special Key `source` can be used to include other config files. The 221 | Value for this must be a path. 222 | 223 | - Anything after the first Whitespace is considered the Value. 224 | 225 | - Any character except NULL bytes are allowed in the Value. 226 | - The Value is optional. 227 | 228 | - All Lines that start with one or more Whitespace characters will be appended 229 | to the last Value, even if there are blank lines or comments in between. The 230 | leading whitespace will be removed. 231 | 232 | Alternatives 233 | ------------ 234 | 235 | Aside from those mentioned in the "But why not..." section above: 236 | 237 | - [github.com/kovetskiy/ko](https://github.com/kovetskiy/ko) 238 | - [github.com/stevenroose/gonfig](https://github.com/stevenroose/gonfig) 239 | 240 | Probably others? Open an issue/PR and I'll add it. 241 | 242 | 243 | [json]: http://www.arp242.net/json-config.html 244 | [yaml]: http://www.arp242.net/yaml-config.html 245 | [zvalidate]: https://github.com/arp242/zvalidate 246 | 247 | -------------------------------------------------------------------------------- /example.config: -------------------------------------------------------------------------------- 1 | # This is a comment 2 | 3 | port 8080 # This is also a comment 4 | 5 | # Look ma, no quotes! 6 | base-url http://example.com 7 | 8 | # We'll parse these in a []*regexp.Regexp 9 | match ^foo.+ 10 | match ^b[ao]r 11 | 12 | # Two values 13 | order allow deny 14 | 15 | host # Idented lines are collapsed 16 | arp242.net # My website 17 | goatcounter.com # My other website 18 | 19 | address arp242.net 20 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package sconfig_test 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "regexp" 7 | 8 | "zgo.at/sconfig" 9 | _ "zgo.at/sconfig/handlers/regexp" 10 | ) 11 | 12 | type Config struct { 13 | Port int64 14 | BaseURL string 15 | Match []*regexp.Regexp 16 | Order []string 17 | Hosts []string 18 | Address string 19 | } 20 | 21 | func Example() { 22 | config := Config{} 23 | err := sconfig.Parse(&config, "example.config", sconfig.Handlers{ 24 | // Custom handler 25 | "address": func(line []string) error { 26 | addr, err := net.LookupHost(line[0]) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | config.Address = addr[0] 32 | return nil 33 | }, 34 | }) 35 | if err != nil { 36 | panic(fmt.Errorf("error parsing config: %s", err)) 37 | } 38 | 39 | fmt.Printf("%+v\n", config) 40 | 41 | // Output: {Port:8080 BaseURL:http://example.com Match:[^foo.+ ^b[ao]r] Order:[allow deny] Hosts:[arp242.net goatcounter.com] Address:arp242.net} 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module zgo.at/sconfig 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package sconfig 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // This file contains the default handler functions for Go's primitives. 10 | 11 | func init() { 12 | defaultTypeHandlers() 13 | } 14 | 15 | func defaultTypeHandlers() { 16 | typeHandlers = map[string][]TypeHandler{ 17 | "string": {handleString}, 18 | "bool": {handleBool}, 19 | "float32": {ValidateSingleValue(), handleFloat32}, 20 | "float64": {ValidateSingleValue(), handleFloat64}, 21 | "int64": {ValidateSingleValue(), handleInt64}, 22 | "uint64": {ValidateSingleValue(), handleUint64}, 23 | "[]string": {ValidateValueLimit(1, 0), handleStringSlice}, 24 | "[]bool": {ValidateValueLimit(1, 0), handleBoolSlice}, 25 | "[]float32": {ValidateValueLimit(1, 0), handleFloat32Slice}, 26 | "[]float64": {ValidateValueLimit(1, 0), handleFloat64Slice}, 27 | "[]int64": {ValidateValueLimit(1, 0), handleInt64Slice}, 28 | "[]uint64": {ValidateValueLimit(1, 0), handleUint64Slice}, 29 | "map[string]string": {ValidateValueLimit(2, 0), handleStringMap}, 30 | } 31 | } 32 | 33 | func handleString(v []string) (interface{}, error) { 34 | return strings.Join(v, " "), nil 35 | } 36 | 37 | func handleBool(v []string) (interface{}, error) { 38 | r, err := parseBool(strings.Join(v, "")) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return r, nil 43 | } 44 | 45 | func parseBool(v string) (bool, error) { 46 | switch strings.ToLower(v) { 47 | case "1", "true", "yes", "on", "enable", "enabled", "": 48 | return true, nil 49 | case "0", "false", "no", "off", "disable", "disabled": 50 | return false, nil 51 | default: 52 | return false, fmt.Errorf(`unable to parse "%s" as a boolean`, v) 53 | } 54 | } 55 | 56 | func handleFloat32(v []string) (interface{}, error) { 57 | r, err := strconv.ParseFloat(strings.Join(v, ""), 32) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return float32(r), nil 62 | } 63 | func handleFloat64(v []string) (interface{}, error) { 64 | r, err := strconv.ParseFloat(strings.Join(v, ""), 64) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return r, nil 69 | } 70 | 71 | func handleInt64(v []string) (interface{}, error) { 72 | r, err := strconv.ParseInt(strings.Join(v, ""), 10, 64) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return r, nil 77 | } 78 | 79 | func handleUint64(v []string) (interface{}, error) { 80 | r, err := strconv.ParseUint(strings.Join(v, ""), 10, 64) 81 | if err != nil { 82 | return nil, err 83 | } 84 | return r, nil 85 | } 86 | 87 | func handleStringSlice(v []string) (interface{}, error) { 88 | return v, nil 89 | } 90 | 91 | func handleBoolSlice(v []string) (interface{}, error) { 92 | a := make([]bool, len(v)) 93 | for i := range v { 94 | r, err := parseBool(v[i]) 95 | if err != nil { 96 | return nil, err 97 | } 98 | a[i] = r 99 | } 100 | return a, nil 101 | } 102 | 103 | func handleFloat32Slice(v []string) (interface{}, error) { 104 | a := make([]float32, len(v)) 105 | for i := range v { 106 | r, err := strconv.ParseFloat(v[i], 32) 107 | if err != nil { 108 | return nil, err 109 | } 110 | a[i] = float32(r) 111 | } 112 | return a, nil 113 | } 114 | 115 | func handleFloat64Slice(v []string) (interface{}, error) { 116 | a := make([]float64, len(v)) 117 | for i := range v { 118 | r, err := strconv.ParseFloat(v[i], 64) 119 | if err != nil { 120 | return nil, err 121 | } 122 | a[i] = r 123 | } 124 | return a, nil 125 | } 126 | 127 | func handleInt64Slice(v []string) (interface{}, error) { 128 | a := make([]int64, len(v)) 129 | for i := range v { 130 | r, err := strconv.ParseInt(v[i], 10, 64) 131 | if err != nil { 132 | return nil, err 133 | } 134 | a[i] = r 135 | } 136 | return a, nil 137 | } 138 | 139 | func handleUint64Slice(v []string) (interface{}, error) { 140 | a := make([]uint64, len(v)) 141 | for i := range v { 142 | r, err := strconv.ParseUint(v[i], 10, 64) 143 | if err != nil { 144 | return nil, err 145 | } 146 | a[i] = r 147 | } 148 | return a, nil 149 | } 150 | 151 | func handleStringMap(v []string) (interface{}, error) { 152 | if len(v)%2 != 0 { 153 | return nil, fmt.Errorf("uneven number of arguments: %d", len(v)) 154 | } 155 | 156 | a := make(map[string]string, len(v)/2) 157 | k := "" 158 | for i := range v { 159 | if i%2 == 0 { 160 | k = v[i] 161 | } else { 162 | a[k] = v[i] 163 | } 164 | } 165 | 166 | return a, nil 167 | } 168 | -------------------------------------------------------------------------------- /handlers/big/big.go: -------------------------------------------------------------------------------- 1 | // Package big contains handlers for parsing values with the math/big package. 2 | // 3 | // It currently implements the big.Int and big.Float types. 4 | package big 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | "strings" 10 | 11 | "zgo.at/sconfig" 12 | ) 13 | 14 | var ( 15 | errHandleInt = "unable to convert %v to big.Int" 16 | errHandleFloat = "unable to convert %v to big.Float" 17 | ) 18 | 19 | func init() { 20 | sconfig.RegisterType("*big.Int", sconfig.ValidateSingleValue(), handleInt) 21 | sconfig.RegisterType("*big.Float", sconfig.ValidateSingleValue(), handleFloat) 22 | sconfig.RegisterType("[]*big.Int", sconfig.ValidateValueLimit(1, 0), handleIntSlice) 23 | sconfig.RegisterType("[]*big.Float", sconfig.ValidateValueLimit(1, 0), handleFloatSlice) 24 | } 25 | 26 | func handleInt(v []string) (interface{}, error) { 27 | n := big.Int{} 28 | z, success := n.SetString(strings.Join(v, ""), 10) 29 | if !success { 30 | return nil, fmt.Errorf(errHandleInt, strings.Join(v, "")) 31 | } 32 | return z, nil 33 | } 34 | 35 | func handleFloat(v []string) (interface{}, error) { 36 | n := big.Float{} 37 | z, success := n.SetString(strings.Join(v, "")) 38 | if !success { 39 | return nil, fmt.Errorf(errHandleFloat, strings.Join(v, "")) 40 | } 41 | return z, nil 42 | } 43 | 44 | func handleIntSlice(v []string) (interface{}, error) { 45 | a := make([]*big.Int, len(v)) 46 | for i := range v { 47 | a[i] = &big.Int{} 48 | z, success := a[i].SetString(v[i], 10) 49 | if !success { 50 | return nil, fmt.Errorf(errHandleInt, v[i]) 51 | } 52 | a[i] = z 53 | } 54 | return a, nil 55 | } 56 | 57 | func handleFloatSlice(v []string) (interface{}, error) { 58 | a := make([]*big.Float, len(v)) 59 | for i := range v { 60 | a[i] = &big.Float{} 61 | z, success := a[i].SetString(v[i]) 62 | if !success { 63 | return nil, fmt.Errorf(errHandleFloat, v[i]) 64 | } 65 | a[i] = z 66 | } 67 | return a, nil 68 | } 69 | -------------------------------------------------------------------------------- /handlers/big/big_test.go: -------------------------------------------------------------------------------- 1 | package big 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "strings" 7 | "testing" 8 | 9 | "zgo.at/sconfig" 10 | ) 11 | 12 | func TestMath(t *testing.T) { 13 | cases := []struct { 14 | fun sconfig.TypeHandler 15 | in []string 16 | want interface{} 17 | wantErr string 18 | }{ 19 | {handleInt, []string{"42"}, big.NewInt(42), ""}, 20 | {handleInt, []string{"42.1"}, nil, fmt.Sprintf(errHandleInt, 42.1)}, 21 | {handleInt, []string{"9223372036854775808"}, 22 | big.NewInt(0).Add(big.NewInt(9223372036854775807), big.NewInt(1)), 23 | ""}, 24 | 25 | {handleFloat, []string{"42"}, big.NewFloat(42), ""}, 26 | {handleFloat, []string{"42.1"}, big.NewFloat(42.1), ""}, 27 | {handleFloat, []string{"4x"}, nil, fmt.Sprintf(errHandleFloat, "4x")}, 28 | 29 | {handleIntSlice, []string{"100", "101"}, []*big.Int{big.NewInt(100), big.NewInt(101)}, ""}, 30 | {handleIntSlice, []string{"100", "10x1"}, nil, "unable to convert 10x1 to big.Int"}, 31 | {handleFloatSlice, []string{"100", "101"}, []*big.Float{big.NewFloat(100), big.NewFloat(101)}, ""}, 32 | {handleFloatSlice, []string{"100", "10x1"}, nil, "unable to convert 10x1 to big.Float"}, 33 | } 34 | 35 | for i, tc := range cases { 36 | t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { 37 | out, err := tc.fun(tc.in) 38 | if !errorContains(err, tc.wantErr) { 39 | t.Errorf("err wrong\nwant: %v\nout: %v\n", tc.wantErr, err) 40 | } 41 | 42 | o := fmt.Sprintf("%#v", out) 43 | w := fmt.Sprintf("%#v", tc.want) 44 | if o != w { 45 | t.Errorf("\nwant: %#v (%[1]T)\nout: %#v (%[2]T)\n", tc.want, out) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func errorContains(out error, want string) bool { 52 | if out == nil { 53 | return want == "" 54 | } 55 | if want == "" { 56 | return false 57 | } 58 | return strings.Contains(out.Error(), want) 59 | } 60 | -------------------------------------------------------------------------------- /handlers/doc.go: -------------------------------------------------------------------------------- 1 | // Package handlers contains handlers that require extra imports (either from 2 | // the standard library, or third-party packages). 3 | package handlers 4 | -------------------------------------------------------------------------------- /handlers/doc_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | -------------------------------------------------------------------------------- /handlers/html/template/template.go: -------------------------------------------------------------------------------- 1 | // Package template contains handlers for parsing values with the html/template package. 2 | package template 3 | 4 | import ( 5 | "html/template" 6 | "strings" 7 | 8 | "zgo.at/sconfig" 9 | ) 10 | 11 | func init() { 12 | sconfig.RegisterType("template.HTML", handleHTML) 13 | } 14 | 15 | func handleHTML(v []string) (interface{}, error) { 16 | return template.HTML(strings.Join(v, " ")), nil 17 | } 18 | -------------------------------------------------------------------------------- /handlers/html/template/template_test.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "reflect" 7 | "testing" 8 | 9 | "zgo.at/sconfig" 10 | ) 11 | 12 | func TestTemplate(t *testing.T) { 13 | cases := []struct { 14 | fun sconfig.TypeHandler 15 | in []string 16 | expected interface{} 17 | expectedErr error 18 | }{ 19 | {handleHTML, []string{"a"}, template.HTML("a"), nil}, 20 | {handleHTML, []string{"a", "b"}, template.HTML("a b"), nil}, 21 | {handleHTML, []string{""}, template.HTML(""), nil}, 22 | } 23 | 24 | for i, tc := range cases { 25 | t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { 26 | out, err := tc.fun(tc.in) 27 | 28 | switch tc.expectedErr { 29 | case nil: 30 | if err != nil { 31 | t.Errorf("expected err to be nil; is: %#v", err) 32 | } 33 | if !reflect.DeepEqual(out, tc.expected) { 34 | t.Errorf("out wrong\nexpected: %#v\nout: %#v\n", 35 | tc.expected, out) 36 | } 37 | default: 38 | if err.Error() != tc.expectedErr.Error() { 39 | t.Errorf("err wrong\nexpected: %v\nout: %v\n", 40 | tc.expectedErr, err) 41 | } 42 | 43 | if out != nil { 44 | t.Errorf("out should be nil if there's an error") 45 | } 46 | } 47 | 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /handlers/net/net.go: -------------------------------------------------------------------------------- 1 | // Package net contains handlers for parsing values with the net package. 2 | // 3 | // It currently implements the net.IP type. 4 | package net 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "strings" 10 | 11 | "zgo.at/sconfig" 12 | ) 13 | 14 | func init() { 15 | sconfig.RegisterType("net.IP", sconfig.ValidateSingleValue(), handleIP) 16 | sconfig.RegisterType("[]net.IP", sconfig.ValidateValueLimit(1, 0), handleIPSlice) 17 | } 18 | 19 | // handleIP parses an IPv4 or IPv6 address 20 | func handleIP(v []string) (interface{}, error) { 21 | IP, _, err := net.ParseCIDR(strings.Join(v, "")) 22 | if err != nil { 23 | IP = net.ParseIP(v[0]) 24 | } 25 | if IP == nil { 26 | return nil, fmt.Errorf("not a valid IP address: %v", v[0]) 27 | } 28 | return IP, nil 29 | } 30 | 31 | func handleIPSlice(v []string) (interface{}, error) { 32 | a := make([]net.IP, len(v)) 33 | for i := range v { 34 | ip, err := handleIP([]string{v[i]}) 35 | if err != nil { 36 | return nil, err 37 | } 38 | a[i] = ip.(net.IP) 39 | } 40 | return a, nil 41 | } 42 | -------------------------------------------------------------------------------- /handlers/net/net_test.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | 10 | "zgo.at/sconfig" 11 | ) 12 | 13 | func TestNet(t *testing.T) { 14 | cases := []struct { 15 | fun sconfig.TypeHandler 16 | in []string 17 | want interface{} 18 | wantErr string 19 | }{ 20 | { 21 | handleIP, []string{"127.0.0.1"}, 22 | net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0x7f, 0x0, 0x0, 0x1}, 23 | "", 24 | }, 25 | { 26 | handleIP, []string{"127.0.0.1X"}, 27 | nil, "not a valid IP address: 127.0.0.1X", 28 | }, 29 | { 30 | handleIPSlice, []string{"127.0.0.1", "192.168.0.1"}, 31 | []net.IP{ 32 | {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0x7f, 0x0, 0x0, 0x1}, 33 | {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xc0, 0xa8, 0x0, 0x1}, 34 | }, 35 | "", 36 | }, 37 | { 38 | handleIPSlice, []string{"127.0.0.1", "127.0.0.1X"}, 39 | nil, "not a valid IP address: 127.0.0.1X", 40 | }, 41 | } 42 | 43 | for i, tc := range cases { 44 | t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { 45 | out, err := tc.fun(tc.in) 46 | if !errorContains(err, tc.wantErr) { 47 | t.Errorf("err wrong\nwant: %v\nout: %v\n", tc.wantErr, err) 48 | } 49 | 50 | if !reflect.DeepEqual(out, tc.want) { 51 | t.Errorf("\nwant: %#v\nout: %#v\n", tc.want, out) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func errorContains(out error, want string) bool { 58 | if out == nil { 59 | return want == "" 60 | } 61 | if want == "" { 62 | return false 63 | } 64 | return strings.Contains(out.Error(), want) 65 | } 66 | -------------------------------------------------------------------------------- /handlers/net/url/url.go: -------------------------------------------------------------------------------- 1 | // Package url contains handlers for parsing values with the net/url package. 2 | // 3 | // It currently implements the url.URL type. Note Go's url package does not do a 4 | // lot of validation, and will happily "parse" wildly invalid URLs without 5 | // returning an error. 6 | package url 7 | 8 | import ( 9 | "net/url" 10 | "strings" 11 | 12 | "zgo.at/sconfig" 13 | ) 14 | 15 | func init() { 16 | sconfig.RegisterType("*url.URL", sconfig.ValidateSingleValue(), handleURL) 17 | sconfig.RegisterType("[]*url.URL", sconfig.ValidateValueLimit(1, 0), handleURLSlice) 18 | } 19 | 20 | func handleURL(v []string) (interface{}, error) { 21 | u, err := url.Parse(strings.Join(v, "")) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return u, nil 26 | } 27 | 28 | func handleURLSlice(v []string) (interface{}, error) { 29 | a := make([]*url.URL, len(v)) 30 | for i := range v { 31 | u, err := url.Parse(v[i]) 32 | if err != nil { 33 | return nil, err 34 | } 35 | a[i] = u 36 | } 37 | return a, nil 38 | } 39 | -------------------------------------------------------------------------------- /handlers/net/url/url_test.go: -------------------------------------------------------------------------------- 1 | package url 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | 10 | "zgo.at/sconfig" 11 | ) 12 | 13 | func TestURL(t *testing.T) { 14 | cases := []struct { 15 | fun sconfig.TypeHandler 16 | in []string 17 | want interface{} 18 | wantErr string 19 | }{ 20 | {handleURL, []string{"%"}, nil, "invalid URL escape"}, 21 | {handleURL, []string{"http://example.com/path"}, &url.URL{ 22 | Scheme: "http", 23 | Host: "example.com", 24 | Path: "/path", 25 | }, ""}, 26 | 27 | {handleURLSlice, []string{"http://example.com/path", "https://example.net"}, []*url.URL{ 28 | {Scheme: "http", Host: "example.com", Path: "/path"}, 29 | {Scheme: "https", Host: "example.net"}, 30 | }, ""}, 31 | {handleURLSlice, []string{"example.com", "%"}, nil, "invalid URL escape"}, 32 | } 33 | 34 | for i, tc := range cases { 35 | t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { 36 | out, err := tc.fun(tc.in) 37 | if !errorContains(err, tc.wantErr) { 38 | t.Errorf("err wrong\nwant: %v\nout: %v\n", tc.wantErr, err) 39 | } 40 | if !reflect.DeepEqual(out, tc.want) { 41 | t.Errorf("\nwant: %#v\nout: %#v\n", tc.want, out) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func errorContains(out error, want string) bool { 48 | if out == nil { 49 | return want == "" 50 | } 51 | if want == "" { 52 | return false 53 | } 54 | return strings.Contains(out.Error(), want) 55 | } 56 | -------------------------------------------------------------------------------- /handlers/regexp/regexp.go: -------------------------------------------------------------------------------- 1 | // Package regexp contains handlers for parsing values with the regexp package. 2 | // 3 | // It currently implements the regexp.Regexp types. 4 | package regexp 5 | 6 | import ( 7 | "regexp" 8 | "strings" 9 | 10 | "zgo.at/sconfig" 11 | ) 12 | 13 | func init() { 14 | sconfig.RegisterType("*regexp.Regexp", sconfig.ValidateSingleValue(), handleRegexp) 15 | sconfig.RegisterType("[]*regexp.Regexp", sconfig.ValidateValueLimit(1, 0), handleRegexpSlice) 16 | } 17 | 18 | func handleRegexp(v []string) (interface{}, error) { 19 | r, err := regexp.Compile(strings.Join(v, "")) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return r, nil 25 | } 26 | 27 | func handleRegexpSlice(v []string) (interface{}, error) { 28 | a := make([]*regexp.Regexp, len(v)) 29 | for i := range v { 30 | r, err := regexp.Compile(v[i]) 31 | if err != nil { 32 | return nil, err 33 | } 34 | a[i] = r 35 | } 36 | return a, nil 37 | } 38 | -------------------------------------------------------------------------------- /handlers/regexp/regexp_test.go: -------------------------------------------------------------------------------- 1 | package regexp 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "regexp" 8 | "testing" 9 | 10 | "zgo.at/sconfig" 11 | ) 12 | 13 | func TestRegexp(t *testing.T) { 14 | cases := []struct { 15 | fun sconfig.TypeHandler 16 | in []string 17 | expected interface{} 18 | expectedErr error 19 | }{ 20 | {handleRegexp, []string{"a"}, regexp.MustCompile(`a`), nil}, 21 | {handleRegexp, []string{"[", "A-Z", "]"}, regexp.MustCompile("[A-Z]"), nil}, 22 | {handleRegexp, []string{"("}, nil, errors.New("error parsing regexp: missing closing ): `(`")}, 23 | {handleRegexp, []string{"[", "a-z", "0-9", "]"}, regexp.MustCompile("[a-z0-9]"), nil}, 24 | 25 | { 26 | handleRegexpSlice, 27 | []string{"[a-z]", "[0-9]"}, 28 | []*regexp.Regexp{regexp.MustCompile("[a-z]"), regexp.MustCompile("[0-9]")}, 29 | nil, 30 | }, 31 | { 32 | handleRegexpSlice, 33 | []string{"[a-z]", "[0-9"}, 34 | nil, 35 | errors.New("error parsing regexp: missing closing ]: `[0-9`"), 36 | }, 37 | { 38 | handleRegexpSlice, 39 | []string{"[a-z", "[0-9]"}, 40 | nil, 41 | errors.New("error parsing regexp: missing closing ]: `[a-z`"), 42 | }, 43 | } 44 | 45 | for i, tc := range cases { 46 | t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { 47 | out, err := tc.fun(tc.in) 48 | 49 | switch tc.expectedErr { 50 | case nil: 51 | if err != nil { 52 | t.Errorf("expected err to be nil; is: %#v", err) 53 | } 54 | if !reflect.DeepEqual(out, tc.expected) { 55 | t.Errorf("out wrong\nexpected: %#v\nout: %#v\n", 56 | tc.expected, out) 57 | } 58 | default: 59 | if err.Error() != tc.expectedErr.Error() { 60 | t.Errorf("err wrong\nexpected: %v\nout: %v\n", 61 | tc.expectedErr, err) 62 | } 63 | 64 | if out != nil { 65 | t.Errorf("out should be nil if there's an error") 66 | } 67 | } 68 | 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /handlers_test.go: -------------------------------------------------------------------------------- 1 | package sconfig 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestHandlers(t *testing.T) { 11 | cases := []struct { 12 | fun TypeHandler 13 | in []string 14 | want interface{} 15 | wantErr string 16 | }{ 17 | {handleString, []string{}, "", ""}, 18 | {handleString, []string{"H€llo"}, "H€llo", ""}, 19 | {handleString, []string{"Hello", "world!"}, "Hello world!", ""}, 20 | {handleString, []string{"3.14"}, "3.14", ""}, 21 | 22 | {handleBool, []string{"false"}, false, ""}, 23 | {handleBool, []string{"TRUE"}, true, ""}, 24 | {handleBool, []string{"enabl", "ed"}, true, ""}, 25 | {handleBool, []string{}, true, ""}, 26 | {handleBool, []string{"it is true"}, nil, `unable to parse "it is true" as a boolean`}, 27 | 28 | {handleFloat32, []string{}, nil, `strconv.ParseFloat: parsing "": invalid syntax`}, 29 | {handleFloat32, []string{"0.0"}, float32(0.0), ""}, 30 | {handleFloat32, []string{".000001"}, float32(0.000001), ""}, 31 | {handleFloat32, []string{"1"}, float32(1), ""}, 32 | {handleFloat32, []string{"1.1", "12"}, float32(1.112), ""}, 33 | 34 | {handleFloat64, []string{}, nil, `strconv.ParseFloat: parsing "": invalid syntax`}, 35 | {handleFloat64, []string{"0.0"}, float64(0.0), ""}, 36 | {handleFloat64, []string{".000001"}, float64(0.000001), ""}, 37 | {handleFloat64, []string{"1"}, float64(1), ""}, 38 | {handleFloat64, []string{"1.1", "12"}, float64(1.112), ""}, 39 | 40 | {handleStringMap, []string{"a", "b"}, map[string]string{"a": "b"}, ""}, 41 | {handleStringMap, []string{"a", "b", "x", "y"}, map[string]string{"a": "b", "x": "y"}, ""}, 42 | {handleStringMap, []string{"a", "b", "x"}, nil, "uneven number of arguments: 3"}, 43 | } 44 | 45 | for i, tc := range cases { 46 | t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { 47 | out, err := tc.fun(tc.in) 48 | if !errorContains(err, tc.wantErr) { 49 | t.Errorf("err wrong\nwant: %v\nout: %v\n", tc.wantErr, err) 50 | } 51 | if !reflect.DeepEqual(out, tc.want) { 52 | t.Errorf("\nwant: %#v\nout: %#v\n", tc.want, out) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func errorContains(out error, want string) bool { 59 | if out == nil { 60 | return want == "" 61 | } 62 | if want == "" { 63 | return false 64 | } 65 | return strings.Contains(out.Error(), want) 66 | } 67 | -------------------------------------------------------------------------------- /inflect.go: -------------------------------------------------------------------------------- 1 | // This is bitbucket.org/pkg/inflect stripped down to the parts that we need 2 | // (Camelize and Pluralize) and modified somewhat to fit better with the sconfig 3 | // API. 4 | // 5 | // Copyright © 2011 Chris Farmiloe 6 | // Copyright © 2017 Martin Tournoij 7 | // See the bottom of this file for the full copyright. 8 | // 9 | // TODO: Add tests, too 10 | // TODO: Probably want to export some of this, so users can add their own. 11 | 12 | package sconfig 13 | 14 | import ( 15 | "strings" 16 | "unicode" 17 | "unicode/utf8" 18 | ) 19 | 20 | // inflectSet is the configuration for the pluralization rules. You can extend 21 | // the rules with the Add* methods. 22 | type inflectSet struct { 23 | uncountables map[string]bool 24 | plurals []*rule 25 | singulars []*rule 26 | humans []*rule 27 | //acronyms []*rule 28 | //acronymMatcher *regexp.Regexp 29 | } 30 | 31 | type rule struct { 32 | suffix string 33 | replacement string 34 | exact bool 35 | } 36 | 37 | var inflect *inflectSet 38 | 39 | func init() { 40 | inflect = newRuleset() 41 | setDefault(inflect) 42 | } 43 | 44 | // NewInflectSet create a blank InflectSet. Unless you are going to build your 45 | // own rules from scratch you probably won't need this and can just use the 46 | // defaultRuleset via the global inflect.* methods. 47 | func newRuleset() *inflectSet { 48 | rs := new(inflectSet) 49 | rs.uncountables = make(map[string]bool) 50 | rs.plurals = make([]*rule, 0) 51 | rs.singulars = make([]*rule, 0) 52 | rs.humans = make([]*rule, 0) 53 | //rs.acronyms = make([]*rule, 0) 54 | return rs 55 | } 56 | 57 | // setDefault sets the default set of common English pluralization rules for an 58 | // inflectSet. 59 | func setDefault(rs *inflectSet) { 60 | rs.AddPlural("s", "s") 61 | rs.AddPlural("testis", "testes") 62 | rs.AddPlural("axis", "axes") 63 | rs.AddPlural("octopus", "octopi") 64 | rs.AddPlural("virus", "viri") 65 | rs.AddPlural("octopi", "octopi") 66 | rs.AddPlural("viri", "viri") 67 | rs.AddPlural("alias", "aliases") 68 | rs.AddPlural("status", "statuses") 69 | rs.AddPlural("bus", "buses") 70 | rs.AddPlural("buffalo", "buffaloes") 71 | rs.AddPlural("tomato", "tomatoes") 72 | rs.AddPlural("tum", "ta") 73 | rs.AddPlural("ium", "ia") 74 | rs.AddPlural("ta", "ta") 75 | rs.AddPlural("ia", "ia") 76 | rs.AddPlural("sis", "ses") 77 | rs.AddPlural("lf", "lves") 78 | rs.AddPlural("rf", "rves") 79 | rs.AddPlural("afe", "aves") 80 | rs.AddPlural("bfe", "bves") 81 | rs.AddPlural("cfe", "cves") 82 | rs.AddPlural("dfe", "dves") 83 | rs.AddPlural("efe", "eves") 84 | rs.AddPlural("gfe", "gves") 85 | rs.AddPlural("hfe", "hves") 86 | rs.AddPlural("ife", "ives") 87 | rs.AddPlural("jfe", "jves") 88 | rs.AddPlural("kfe", "kves") 89 | rs.AddPlural("lfe", "lves") 90 | rs.AddPlural("mfe", "mves") 91 | rs.AddPlural("nfe", "nves") 92 | rs.AddPlural("ofe", "oves") 93 | rs.AddPlural("pfe", "pves") 94 | rs.AddPlural("qfe", "qves") 95 | rs.AddPlural("rfe", "rves") 96 | rs.AddPlural("sfe", "sves") 97 | rs.AddPlural("tfe", "tves") 98 | rs.AddPlural("ufe", "uves") 99 | rs.AddPlural("vfe", "vves") 100 | rs.AddPlural("wfe", "wves") 101 | rs.AddPlural("xfe", "xves") 102 | rs.AddPlural("yfe", "yves") 103 | rs.AddPlural("zfe", "zves") 104 | rs.AddPlural("hive", "hives") 105 | rs.AddPlural("quy", "quies") 106 | rs.AddPlural("by", "bies") 107 | rs.AddPlural("cy", "cies") 108 | rs.AddPlural("dy", "dies") 109 | rs.AddPlural("fy", "fies") 110 | rs.AddPlural("gy", "gies") 111 | rs.AddPlural("hy", "hies") 112 | rs.AddPlural("jy", "jies") 113 | rs.AddPlural("ky", "kies") 114 | rs.AddPlural("ly", "lies") 115 | rs.AddPlural("my", "mies") 116 | rs.AddPlural("ny", "nies") 117 | rs.AddPlural("py", "pies") 118 | rs.AddPlural("qy", "qies") 119 | rs.AddPlural("ry", "ries") 120 | rs.AddPlural("sy", "sies") 121 | rs.AddPlural("ty", "ties") 122 | rs.AddPlural("vy", "vies") 123 | rs.AddPlural("wy", "wies") 124 | rs.AddPlural("xy", "xies") 125 | rs.AddPlural("zy", "zies") 126 | rs.AddPlural("x", "xes") 127 | rs.AddPlural("ch", "ches") 128 | rs.AddPlural("ss", "sses") 129 | rs.AddPlural("sh", "shes") 130 | rs.AddPlural("matrix", "matrices") 131 | rs.AddPlural("vertix", "vertices") 132 | rs.AddPlural("indix", "indices") 133 | rs.AddPlural("matrex", "matrices") 134 | rs.AddPlural("vertex", "vertices") 135 | rs.AddPlural("index", "indices") 136 | rs.AddPlural("mouse", "mice") 137 | rs.AddPlural("louse", "lice") 138 | rs.AddPlural("mice", "mice") 139 | rs.AddPlural("lice", "lice") 140 | rs.AddPluralExact("ox", "oxen", true) 141 | rs.AddPluralExact("oxen", "oxen", true) 142 | rs.AddPluralExact("quiz", "quizzes", true) 143 | rs.AddSingular("s", "") 144 | rs.AddSingular("news", "news") 145 | rs.AddSingular("ta", "tum") 146 | rs.AddSingular("ia", "ium") 147 | rs.AddSingular("analyses", "analysis") 148 | rs.AddSingular("bases", "basis") 149 | rs.AddSingular("diagnoses", "diagnosis") 150 | rs.AddSingular("parentheses", "parenthesis") 151 | rs.AddSingular("prognoses", "prognosis") 152 | rs.AddSingular("synopses", "synopsis") 153 | rs.AddSingular("theses", "thesis") 154 | rs.AddSingular("analyses", "analysis") 155 | rs.AddSingular("aves", "afe") 156 | rs.AddSingular("bves", "bfe") 157 | rs.AddSingular("cves", "cfe") 158 | rs.AddSingular("dves", "dfe") 159 | rs.AddSingular("eves", "efe") 160 | rs.AddSingular("gves", "gfe") 161 | rs.AddSingular("hves", "hfe") 162 | rs.AddSingular("ives", "ife") 163 | rs.AddSingular("jves", "jfe") 164 | rs.AddSingular("kves", "kfe") 165 | rs.AddSingular("lves", "lfe") 166 | rs.AddSingular("mves", "mfe") 167 | rs.AddSingular("nves", "nfe") 168 | rs.AddSingular("oves", "ofe") 169 | rs.AddSingular("pves", "pfe") 170 | rs.AddSingular("qves", "qfe") 171 | rs.AddSingular("rves", "rfe") 172 | rs.AddSingular("sves", "sfe") 173 | rs.AddSingular("tves", "tfe") 174 | rs.AddSingular("uves", "ufe") 175 | rs.AddSingular("vves", "vfe") 176 | rs.AddSingular("wves", "wfe") 177 | rs.AddSingular("xves", "xfe") 178 | rs.AddSingular("yves", "yfe") 179 | rs.AddSingular("zves", "zfe") 180 | rs.AddSingular("hives", "hive") 181 | rs.AddSingular("tives", "tive") 182 | rs.AddSingular("lves", "lf") 183 | rs.AddSingular("rves", "rf") 184 | rs.AddSingular("quies", "quy") 185 | rs.AddSingular("bies", "by") 186 | rs.AddSingular("cies", "cy") 187 | rs.AddSingular("dies", "dy") 188 | rs.AddSingular("fies", "fy") 189 | rs.AddSingular("gies", "gy") 190 | rs.AddSingular("hies", "hy") 191 | rs.AddSingular("jies", "jy") 192 | rs.AddSingular("kies", "ky") 193 | rs.AddSingular("lies", "ly") 194 | rs.AddSingular("mies", "my") 195 | rs.AddSingular("nies", "ny") 196 | rs.AddSingular("pies", "py") 197 | rs.AddSingular("qies", "qy") 198 | rs.AddSingular("ries", "ry") 199 | rs.AddSingular("sies", "sy") 200 | rs.AddSingular("ties", "ty") 201 | rs.AddSingular("vies", "vy") 202 | rs.AddSingular("wies", "wy") 203 | rs.AddSingular("xies", "xy") 204 | rs.AddSingular("zies", "zy") 205 | rs.AddSingular("series", "series") 206 | rs.AddSingular("movies", "movie") 207 | rs.AddSingular("xes", "x") 208 | rs.AddSingular("ches", "ch") 209 | rs.AddSingular("sses", "ss") 210 | rs.AddSingular("shes", "sh") 211 | rs.AddSingular("mice", "mouse") 212 | rs.AddSingular("lice", "louse") 213 | rs.AddSingular("buses", "bus") 214 | rs.AddSingular("oes", "o") 215 | rs.AddSingular("shoes", "shoe") 216 | rs.AddSingular("crises", "crisis") 217 | rs.AddSingular("axes", "axis") 218 | rs.AddSingular("testes", "testis") 219 | rs.AddSingular("octopi", "octopus") 220 | rs.AddSingular("viri", "virus") 221 | rs.AddSingular("statuses", "status") 222 | rs.AddSingular("aliases", "alias") 223 | rs.AddSingularExact("oxen", "ox", true) 224 | rs.AddSingular("vertices", "vertex") 225 | rs.AddSingular("indices", "index") 226 | rs.AddSingular("matrices", "matrix") 227 | rs.AddSingularExact("quizzes", "quiz", true) 228 | rs.AddSingular("databases", "database") 229 | rs.AddIrregular("person", "people") 230 | rs.AddIrregular("man", "men") 231 | rs.AddIrregular("child", "children") 232 | rs.AddIrregular("sex", "sexes") 233 | rs.AddIrregular("move", "moves") 234 | rs.AddIrregular("zombie", "zombies") 235 | rs.AddUncountable("equipment") 236 | rs.AddUncountable("information") 237 | rs.AddUncountable("rice") 238 | rs.AddUncountable("money") 239 | rs.AddUncountable("species") 240 | rs.AddUncountable("series") 241 | rs.AddUncountable("fish") 242 | rs.AddUncountable("sheep") 243 | rs.AddUncountable("jeans") 244 | rs.AddUncountable("police") 245 | } 246 | 247 | // "dino_party" -> "DinoParty" 248 | func (rs *inflectSet) camelize(word string) string { 249 | words := splitAtCaseChangeWithTitlecase(word) 250 | return strings.Join(words, "") 251 | } 252 | 253 | // returns the plural form of a singular word 254 | func (rs *inflectSet) pluralize(word string) string { 255 | if len(word) == 0 { 256 | return word 257 | } 258 | if rs.isUncountable(word) { 259 | return word 260 | } 261 | for _, rule := range rs.plurals { 262 | if rule.exact { 263 | if word == rule.suffix { 264 | return rule.replacement 265 | } 266 | } else { 267 | if strings.HasSuffix(word, rule.suffix) { 268 | return replaceLast(word, rule.suffix, rule.replacement) 269 | } 270 | } 271 | } 272 | return word + "s" 273 | } 274 | 275 | // returns the singular form of a plural word 276 | func (rs *inflectSet) singularize(word string) string { 277 | if len(word) == 0 { 278 | return word 279 | } 280 | if rs.isUncountable(word) { 281 | return word 282 | } 283 | for _, rule := range rs.singulars { 284 | if rule.exact { 285 | if word == rule.suffix { 286 | return rule.replacement 287 | } 288 | } else { 289 | if strings.HasSuffix(word, rule.suffix) { 290 | return replaceLast(word, rule.suffix, rule.replacement) 291 | } 292 | } 293 | } 294 | return word 295 | } 296 | 297 | // togglePlural will return the plural if word is singular, or the singular if 298 | // the word is plural. 299 | func (rs *inflectSet) togglePlural(word string) string { 300 | toggle := rs.pluralize(word) 301 | if toggle == word { 302 | toggle = rs.singularize(word) 303 | } 304 | 305 | return toggle 306 | } 307 | 308 | // Add a pluralization rule. 309 | func (rs *inflectSet) AddPlural(suffix, replacement string) { 310 | rs.AddPluralExact(suffix, replacement, false) 311 | } 312 | 313 | // Add a pluralization rule with full string match. 314 | func (rs *inflectSet) AddPluralExact(suffix, replacement string, exact bool) { 315 | // remove uncountable 316 | delete(rs.uncountables, suffix) 317 | // create rule 318 | r := new(rule) 319 | r.suffix = suffix 320 | r.replacement = replacement 321 | r.exact = exact 322 | // prepend 323 | rs.plurals = append([]*rule{r}, rs.plurals...) 324 | } 325 | 326 | // Add a singular rule. 327 | func (rs *inflectSet) AddSingular(suffix, replacement string) { 328 | rs.AddSingularExact(suffix, replacement, false) 329 | } 330 | 331 | // same as AddSingular but you can set `exact` to force 332 | // a full string match 333 | func (rs *inflectSet) AddSingularExact(suffix, replacement string, exact bool) { 334 | // remove from uncountable 335 | delete(rs.uncountables, suffix) 336 | // create rule 337 | r := new(rule) 338 | r.suffix = suffix 339 | r.replacement = replacement 340 | r.exact = exact 341 | rs.singulars = append([]*rule{r}, rs.singulars...) 342 | } 343 | 344 | // Add any inconsistent pluralizing/sinularizing rules to the set here. 345 | func (rs *inflectSet) AddIrregular(singular, plural string) { 346 | delete(rs.uncountables, singular) 347 | delete(rs.uncountables, plural) 348 | rs.AddPlural(singular, plural) 349 | rs.AddPlural(plural, plural) 350 | rs.AddSingular(plural, singular) 351 | } 352 | 353 | // add a word to this inflectRuleset that has the same singular and plural form 354 | // for example: "rice" 355 | func (rs *inflectSet) AddUncountable(word string) { 356 | rs.uncountables[strings.ToLower(word)] = true 357 | } 358 | 359 | func (rs *inflectSet) isUncountable(word string) bool { 360 | // handle multiple words by using the last one 361 | words := strings.Split(word, " ") 362 | if _, exists := rs.uncountables[strings.ToLower(words[len(words)-1])]; exists { 363 | return true 364 | } 365 | return false 366 | } 367 | 368 | func splitAtCaseChangeWithTitlecase(s string) []string { 369 | words := []string{} 370 | word := []rune{} 371 | 372 | for _, c := range s { 373 | spacer := isSpacerChar(c) 374 | if len(word) > 0 { 375 | if unicode.IsUpper(c) || spacer { 376 | words = append(words, string(word)) 377 | word = make([]rune, 0) 378 | } 379 | } 380 | if !spacer { 381 | if len(word) > 0 { 382 | word = append(word, unicode.ToLower(c)) 383 | } else { 384 | word = append(word, unicode.ToUpper(c)) 385 | } 386 | } 387 | } 388 | words = append(words, string(word)) 389 | return words 390 | } 391 | 392 | func replaceLast(s, match, repl string) string { 393 | // reverse strings 394 | srev := reverse(s) 395 | mrev := reverse(match) 396 | rrev := reverse(repl) 397 | // match first and reverse back 398 | return reverse(strings.Replace(srev, mrev, rrev, 1)) 399 | } 400 | 401 | func isSpacerChar(c rune) bool { 402 | switch { 403 | case c == rune("_"[0]): 404 | return true 405 | case c == rune(" "[0]): 406 | return true 407 | case c == rune(":"[0]): 408 | return true 409 | case c == rune("-"[0]): 410 | return true 411 | } 412 | return false 413 | } 414 | 415 | func reverse(s string) string { 416 | o := make([]rune, utf8.RuneCountInString(s)) 417 | i := len(o) 418 | for _, c := range s { 419 | i-- 420 | o[i] = c 421 | } 422 | return string(o) 423 | } 424 | 425 | // The MIT License (MIT) 426 | // 427 | // Copyright © 2011 Chris Farmiloe 428 | // Copyright © 2016-2017 Martin Tournoij 429 | // 430 | // Permission is hereby granted, free of charge, to any person obtaining a copy 431 | // of this software and associated documentation files (the "Software"), to 432 | // deal in the Software without restriction, including without limitation the 433 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 434 | // sell copies of the Software, and to permit persons to whom the Software is 435 | // furnished to do so, subject to the following conditions: 436 | // 437 | // The above copyright notice and this permission notice shall be included in 438 | // all copies or substantial portions of the Software. 439 | // 440 | // The software is provided "as is", without warranty of any kind, express or 441 | // implied, including but not limited to the warranties of merchantability, 442 | // fitness for a particular purpose and noninfringement. In no event shall the 443 | // authors or copyright holders be liable for any claim, damages or other 444 | // liability, whether in an action of contract, tort or otherwise, arising 445 | // from, out of or in connection with the software or the use or other dealings 446 | // in the software. 447 | -------------------------------------------------------------------------------- /inflect_test.go: -------------------------------------------------------------------------------- 1 | package sconfig 2 | 3 | // TODO 4 | -------------------------------------------------------------------------------- /sconfig.go: -------------------------------------------------------------------------------- 1 | // Package sconfig is a simple yet functional configuration file parser. 2 | // 3 | // See the README.markdown for an introduction. 4 | package sconfig 5 | 6 | import ( 7 | "bufio" 8 | "encoding" 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | "reflect" 13 | "strings" 14 | "unicode" 15 | ) 16 | 17 | var ( 18 | // typeHandlers are all the registered type handlers. 19 | // 20 | // The key is the name of the type, the value the list of handler functions 21 | // to run. 22 | typeHandlers = make(map[string][]TypeHandler) 23 | ) 24 | 25 | // TypeHandler takes the field to set and the value to set it to. It is expected 26 | // to return the value to set it to. 27 | type TypeHandler func([]string) (interface{}, error) 28 | 29 | // Handler functions can be used to run special code for a field. The function 30 | // takes the unprocessed line split by whitespace and with the option name 31 | // removed. 32 | type Handler func([]string) error 33 | 34 | // Handlers can be used to run special code for a field. The map key is the name 35 | // of the field in the struct. 36 | type Handlers map[string]Handler 37 | 38 | // RegisterType sets the type handler functions for a type. Existing handlers 39 | // are always overridden (it doesn't add to the list!) 40 | // 41 | // The handlers are chained; the return value is passed to the next one. The 42 | // chain is stopped if one handler returns a non-nil error. This is particularly 43 | // useful for validation (see ValidateSingleValue() and ValidateValueLimit() for 44 | // examples). 45 | func RegisterType(typ string, fun ...TypeHandler) { 46 | typeHandlers[typ] = fun 47 | } 48 | 49 | // readFile will read a file, strip comments, and collapse indents. This also 50 | // deals with the special "source" command. 51 | // 52 | // The return value is an nested slice where the first item is the original line 53 | // number and the second is the parsed line; for example: 54 | // 55 | // [][]string{ 56 | // []string{3, "key value"}, 57 | // []string{9, "key2 value1 value2"}, 58 | // } 59 | // 60 | // The line numbers can be used later to give more informative error messages. 61 | // 62 | // The input must be utf-8 encoded; other encodings are not supported. 63 | func readFile(file string) (lines [][]string, err error) { 64 | fp, err := os.Open(file) 65 | if err != nil { 66 | return lines, err 67 | } 68 | defer fp.Close() 69 | 70 | i := 0 71 | no := 0 72 | for scanner := bufio.NewScanner(fp); scanner.Scan(); { 73 | no++ 74 | line := scanner.Text() 75 | 76 | isIndented := len(line) > 0 && unicode.IsSpace(rune(line[0])) 77 | line = strings.TrimSpace(line) 78 | 79 | // Skip empty lines and comments 80 | if line == "" || line[0] == '#' { 81 | continue 82 | } 83 | 84 | line = collapseWhitespace(removeComments(line)) 85 | 86 | switch { 87 | // Regular line. 88 | default: 89 | lines = append(lines, []string{fmt.Sprintf("%d", no), line}) 90 | i++ 91 | 92 | // Indented. 93 | case isIndented: 94 | if i == 0 { 95 | return lines, fmt.Errorf("first line can't be indented") 96 | } 97 | // Append to previous line; don't increment i since there may be 98 | // more indented lines. 99 | lines[i-1][1] += " " + strings.TrimSpace(line) 100 | 101 | // Source command. 102 | case strings.HasPrefix(line, "source "): 103 | sourced, err := readFile(line[7:]) 104 | if err != nil { 105 | return nil, err 106 | } 107 | lines = append(lines, sourced...) 108 | i++ 109 | } 110 | } 111 | 112 | return lines, nil 113 | } 114 | 115 | func removeComments(line string) string { 116 | prevcmt := 0 117 | for { 118 | cmt := strings.Index(line[prevcmt:], "#") 119 | if cmt < 0 { 120 | break 121 | } 122 | 123 | cmt += prevcmt 124 | prevcmt = cmt 125 | 126 | // Allow escaping # with \# 127 | if line[cmt-1] == '\\' { 128 | line = line[:cmt-1] + line[cmt:] 129 | } else { 130 | // Found comment, remove the comment text and trailing whitespace. 131 | line = strings.TrimRightFunc(line[:cmt], unicode.IsSpace) 132 | break 133 | } 134 | } 135 | 136 | return line 137 | } 138 | 139 | func collapseWhitespace(line string) string { 140 | nl := "" 141 | prevSpace := false 142 | for i, char := range line { 143 | switch { 144 | case char == '\\': 145 | // \ is escaped with \: "\\" 146 | if line[i-1] == '\\' { 147 | nl += `\` 148 | } 149 | case unicode.IsSpace(char): 150 | if prevSpace { 151 | // Escaped with \: "\ " 152 | if line[i-1] == '\\' { 153 | nl += string(char) 154 | } 155 | } else { 156 | prevSpace = true 157 | if i != len(line)-1 { 158 | nl += " " 159 | } 160 | } 161 | default: 162 | nl += string(char) 163 | prevSpace = false 164 | } 165 | } 166 | 167 | return nl 168 | } 169 | 170 | // MustParse behaves like Parse(), but panics if there is an error. 171 | func MustParse(c interface{}, file string, handlers Handlers) { 172 | err := Parse(c, file, handlers) 173 | if err != nil { 174 | panic(err) 175 | } 176 | } 177 | 178 | // sconfig will intercept panic()s and return them as an error, which is much 179 | // better for most general usage. 180 | // For development it might be useful to disable this though. 181 | var dontPanic = true 182 | 183 | // Parse reads the file from disk and populates the given config struct. 184 | // 185 | // A line is matched with a struct field by "camelizing" the first word. For 186 | // example "key-name" becomes "KeyName". You can also use the plural 187 | // ("KeyNames") as the field name. 188 | // 189 | // sconfig will attempt to set the field from the passed Handlers map (see 190 | // below), a configured type handler, or the encoding.TextUnmarshaler interface, 191 | // in that order. 192 | // 193 | // The Handlers map, which may be nil, can be given to customize the behaviour 194 | // for individual configuration keys. This will override the type handler (if 195 | // any). The function is expected to set any settings on the struct; for 196 | // example: 197 | // 198 | // Parse(&config, "config", Handlers{ 199 | // "SpecialBool": func(line []string) error { 200 | // if line[0] == "yup!" { 201 | // config.Bool = true 202 | // } 203 | // return nil 204 | // }, 205 | // }) 206 | // 207 | // Will allow you to do: 208 | // 209 | // special-bool yup! 210 | func Parse(config interface{}, file string, handlers Handlers) (returnErr error) { 211 | // Recover from panics; return them as errors! 212 | // TODO: This loses the stack though... 213 | defer func() { 214 | if dontPanic { 215 | if rec := recover(); rec != nil { 216 | switch recType := rec.(type) { 217 | case error: 218 | returnErr = recType 219 | default: 220 | panic(rec) 221 | } 222 | } 223 | } 224 | }() 225 | 226 | lines, err := readFile(file) 227 | if err != nil { 228 | return err 229 | } 230 | 231 | values := getValues(config) 232 | 233 | // Get list of rule names from tags 234 | for _, line := range lines { 235 | // Split by spaces 236 | v := strings.Split(line[1], " ") 237 | 238 | var ( 239 | field reflect.Value 240 | fieldName string 241 | ) 242 | switch values.Kind() { 243 | 244 | // TODO: Only support map[string][]string atm. 245 | case reflect.Map: 246 | fieldName = v[0] 247 | mapKey := reflect.ValueOf(v[0]).Convert(reflect.TypeOf(fieldName)) 248 | values.SetMapIndex(mapKey, reflect.ValueOf(v[1:])) 249 | 250 | continue 251 | 252 | case reflect.Struct: 253 | // Infer the field name from the key 254 | var err error 255 | fieldName, err = fieldNameFromKey(v[0], values) 256 | if err != nil { 257 | return fmterr(file, line[0], v[0], err) 258 | } 259 | field = values.FieldByName(fieldName) 260 | 261 | default: 262 | return fmt.Errorf("unknown type: %v", values.Kind()) 263 | } 264 | 265 | // Use the handler if it exists. 266 | if has, err := setFromHandler(fieldName, v[1:], handlers); has { 267 | if err != nil { 268 | return fmterr(file, line[0], v[0], err) 269 | } 270 | continue 271 | } 272 | 273 | // Set from type handler. 274 | if has, err := setFromTypeHandler(&field, v[1:]); has { 275 | if err != nil { 276 | return fmterr(file, line[0], v[0], err) 277 | } 278 | continue 279 | } 280 | 281 | // Set from encoding.TextUnmarshaler. 282 | if m, ok := field.Interface().(encoding.TextUnmarshaler); ok { 283 | if field.IsNil() { 284 | field.Set(reflect.New(field.Type().Elem())) 285 | m = field.Interface().(encoding.TextUnmarshaler) 286 | } 287 | 288 | err := m.UnmarshalText([]byte(strings.Join(v[1:], " "))) 289 | if err != nil { 290 | return fmterr(file, line[0], v[0], err) 291 | } 292 | continue 293 | } 294 | 295 | // Give up :-( 296 | return fmterr(file, line[0], v[0], fmt.Errorf( 297 | "don't know how to set fields of the type %s", 298 | field.Type().String())) 299 | } 300 | 301 | return returnErr // Can be set by defer 302 | } 303 | 304 | // Fields gets a list of all fields in a struct. The map key is the name of the 305 | // field (as it appears in the struct) and the key is the field's reflect.Value 306 | // (which can be used to set a value). 307 | // 308 | // This is useful if you want to batch operate on a config struct, for example 309 | // to override from the environment or flags. 310 | func Fields(config interface{}) map[string]reflect.Value { 311 | r := make(map[string]reflect.Value) 312 | v := reflect.ValueOf(config).Elem() 313 | t := reflect.TypeOf(config).Elem() 314 | for i := 0; i < v.NumField(); i++ { 315 | r[t.Field(i).Name] = v.Field(i) 316 | } 317 | 318 | return r 319 | } 320 | 321 | func getValues(c interface{}) reflect.Value { 322 | // Make sure we give a sane error here when accidentally passing in a 323 | // non-pointer, since the default is not all that helpful: 324 | // panic: reflect: call of reflect.Value.Elem on struct Value 325 | defer func() { 326 | err := recover() 327 | if err != nil { 328 | switch err.(type) { 329 | case *reflect.ValueError: 330 | panic(fmt.Errorf( 331 | "unable to get values of the config struct (did you pass it as a pointer?): %v", 332 | err)) 333 | default: 334 | panic(err) 335 | } 336 | } 337 | }() 338 | return reflect.ValueOf(c).Elem() 339 | } 340 | 341 | func fmterr(file, line, key string, err error) error { 342 | return fmt.Errorf("%v line %v: error parsing %s: %v", 343 | file, line, key, err) 344 | } 345 | 346 | func fieldNameFromKey(key string, values reflect.Value) (string, error) { 347 | fieldName := inflect.camelize(key) 348 | 349 | // This list is from golint 350 | acr := []string{"Api", "Ascii", "Cpu", "Css", "Dns", "Eof", "Guid", "Html", 351 | "Https", "Http", "Id", "Ip", "Json", "Lhs", "Qps", "Ram", "Rhs", 352 | "Rpc", "Sla", "Smtp", "Sql", "Ssh", "Tcp", "Tls", "Ttl", "Udp", 353 | "Ui", "Uid", "Uuid", "Uri", "Url", "Utf8", "Vm", "Xml", "Xsrf", 354 | "Xss"} 355 | for _, a := range acr { 356 | fieldName = strings.Replace(fieldName, a, strings.ToUpper(a), -1) 357 | } 358 | 359 | field := values.FieldByName(fieldName) 360 | if !field.CanAddr() { 361 | // Check plural version too; we're not too fussy 362 | fieldNamePlural := inflect.togglePlural(fieldName) 363 | field = values.FieldByName(fieldNamePlural) 364 | if !field.CanAddr() { 365 | return "", fmt.Errorf("unknown option (field %s or %s is missing)", 366 | fieldName, fieldNamePlural) 367 | } 368 | fieldName = fieldNamePlural 369 | } 370 | 371 | return fieldName, nil 372 | } 373 | 374 | func setFromHandler(fieldName string, values []string, handlers Handlers) (bool, error) { 375 | if handlers == nil { 376 | return false, nil 377 | } 378 | 379 | handler, has := handlers[fieldName] 380 | if !has { 381 | return false, nil 382 | } 383 | 384 | err := handler(values) 385 | if err != nil { 386 | return true, fmt.Errorf("%v (from handler)", err) 387 | } 388 | 389 | return true, nil 390 | } 391 | 392 | func setFromTypeHandler(field *reflect.Value, value []string) (bool, error) { 393 | handler, has := typeHandlers[field.Type().String()] 394 | if !has { 395 | return false, nil 396 | } 397 | 398 | var ( 399 | v interface{} 400 | err error 401 | ) 402 | for _, h := range handler { 403 | v, err = h(value) 404 | if err != nil { 405 | return true, err 406 | } 407 | } 408 | 409 | val := reflect.ValueOf(v) 410 | if field.Kind() == reflect.Slice { 411 | val = reflect.AppendSlice(*field, val) 412 | } 413 | field.Set(val) 414 | return true, nil 415 | } 416 | 417 | // FindConfig tries to find a configuration file at the usual locations. 418 | // 419 | // The following paths are checked (in this order): 420 | // 421 | // $XDG_CONFIG/ 422 | // $HOME/. 423 | // /etc/ 424 | // /usr/local/etc/ 425 | // /usr/pkg/etc/ 426 | // ./ 427 | // 428 | // The default for $XDG_CONFIG is $HOME/.config if it's not set. 429 | func FindConfig(file string) string { 430 | file = strings.TrimLeft(file, "/") 431 | 432 | locations := []string{} 433 | xdg := os.Getenv("XDG_CONFIG") 434 | if xdg != "" { 435 | locations = append(locations, filepath.Join(xdg, file)) 436 | } 437 | if home := os.Getenv("HOME"); home != "" { 438 | if xdg == "" { 439 | locations = append(locations, filepath.Join( 440 | os.Getenv("HOME"), "/.config/", file)) 441 | } 442 | locations = append(locations, home+"/."+file) 443 | } 444 | 445 | locations = append(locations, []string{ 446 | "/etc/" + file, 447 | "/usr/local/etc/" + file, 448 | "/usr/pkg/etc/" + file, 449 | "./" + file, 450 | }...) 451 | 452 | for _, l := range locations { 453 | if _, err := os.Stat(l); err == nil { 454 | return l 455 | } 456 | } 457 | 458 | return "" 459 | } 460 | -------------------------------------------------------------------------------- /sconfig_test.go: -------------------------------------------------------------------------------- 1 | package sconfig 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | // testfile will write data to a temporary file and will return the full file 16 | // path. It is the caller's responsibility to clean the file. 17 | func testfile(data string) (filename string) { 18 | fp, err := ioutil.TempFile(os.TempDir(), "sconfigtest") 19 | if err != nil { 20 | panic(err) 21 | } 22 | defer fp.Close() 23 | 24 | _, err = fp.WriteString(data) 25 | if err != nil { 26 | panic(err) 27 | } 28 | return fp.Name() 29 | } 30 | 31 | func rm(t *testing.T, path string) { 32 | err := os.Remove(path) 33 | if err != nil { 34 | t.Errorf("cannot remove %#v: %v", path, err) 35 | } 36 | } 37 | 38 | func rmAll(t *testing.T, path string) { 39 | err := os.RemoveAll(path) 40 | if err != nil { 41 | t.Errorf("cannot remove %#v: %v", path, err) 42 | } 43 | } 44 | 45 | func TestRegisterType(t *testing.T) { 46 | defer func() { 47 | typeHandlers["int64"] = []TypeHandler{ValidateSingleValue(), handleInt64} 48 | delete(typeHandlers, "int") 49 | }() 50 | 51 | didint := false 52 | didint64 := false 53 | RegisterType("int", func(v []string) (interface{}, error) { 54 | didint = true 55 | return int(42), nil 56 | }) 57 | RegisterType("int64", func(v []string) (interface{}, error) { 58 | didint64 = true 59 | return int64(42), nil 60 | }) 61 | 62 | f := testfile("hello 42\nworld 42") 63 | defer rm(t, f) 64 | 65 | c := &struct { 66 | Hello int64 67 | World int 68 | }{} 69 | 70 | err := Parse(c, f, nil) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | if !didint { 76 | t.Error("didint was not true") 77 | } 78 | if !didint64 { 79 | t.Error("didint64 was not true") 80 | } 81 | } 82 | 83 | func TestReadFileError(t *testing.T) { 84 | // File doesn't exist 85 | out, err := readFile("/nonexistent-file") 86 | if err == nil { 87 | t.Error("no error on reading /nonexistent-file") 88 | } 89 | if len(out) > 0 { 90 | t.Fail() 91 | } 92 | 93 | // Sourced file doesn't exist 94 | f := testfile("source /nonexistent-file") 95 | defer rm(t, f) 96 | out, err = readFile(f) 97 | if err == nil { 98 | t.Error("no error on sourcing /nonexistent-file") 99 | } 100 | if len(out) > 0 { 101 | t.Error("len(out) > 0") 102 | } 103 | 104 | // First line is indented: makes no sense. 105 | f2 := testfile(" indented") 106 | defer rm(t, f2) 107 | out, err = readFile(f2) 108 | if err == nil { 109 | t.Error("no error when first line is indented") 110 | } 111 | if len(out) > 0 { 112 | t.Error("len(out) > 0") 113 | } 114 | } 115 | 116 | func TestReadFile(t *testing.T) { 117 | source := testfile("sourced file") 118 | defer rm(t, source) 119 | 120 | test := fmt.Sprintf(` 121 | # A comment 122 | key value # Ignore this too 123 | 124 | key 125 | value1 # Also ignored 126 | value2 127 | 128 | another−€¡ Hé€ Well... 129 | 130 | collapse many whitespaces 131 | 132 | ig\#nore comments \# like this 133 | 134 | uni-code      white      space     135 | pre_serve \ spaces \ \ like \ \ so 136 | 137 | back s\\la\sh 138 | 139 | 140 | source %v 141 | 142 | `, source) 143 | 144 | expected := [][]string{ 145 | {"3", "key value"}, 146 | {"5", "key value1 value2"}, 147 | {"9", "another−€¡ Hé€ Well..."}, 148 | {"11", "collapse many whitespaces"}, 149 | {"13", "ig#nore comments # like this"}, 150 | {"15", "uni-code white space"}, 151 | {"16", "pre_serve spaces   like so"}, 152 | {"18", `back s\lash`}, 153 | {"1", "sourced file"}, 154 | } 155 | 156 | f := testfile(test) 157 | defer rm(t, f) 158 | out, err := readFile(f) 159 | if err != nil { 160 | t.Errorf("readFile: got err: %v", err) 161 | } 162 | 163 | if len(out) != len(expected) { 164 | t.Logf("len(out) != len(expected)\nout: %#v", out) 165 | t.FailNow() 166 | } 167 | 168 | for i := range expected { 169 | if out[i][0] != expected[i][0] || out[i][1] != expected[i][1] { 170 | t.Errorf("%v failed\nexpected: %#v\nout: %#v\n", 171 | i, expected[i], out[i]) 172 | } 173 | } 174 | } 175 | 176 | func TestFindConfigErrors(t *testing.T) { 177 | f := FindConfig("hieperdepiephoera") 178 | if f != "" { 179 | t.Fail() 180 | } 181 | } 182 | 183 | func TestFindConfig(t *testing.T) { 184 | find := FindConfig("sure_this_wont_exist/anywhere") 185 | if find != "" { 186 | t.Fail() 187 | } 188 | 189 | dir, err := ioutil.TempDir(os.TempDir(), "sconfig_test") 190 | if err != nil { 191 | t.Error(err) 192 | } 193 | defer rmAll(t, dir) 194 | 195 | f, err := ioutil.TempFile(dir, "config") 196 | if err != nil { 197 | t.Fatal(err) 198 | } 199 | 200 | err = os.Setenv("XDG_CONFIG", dir) 201 | if err != nil { 202 | t.Fatal(err) 203 | } 204 | find = FindConfig(filepath.Base(f.Name())) 205 | if find != f.Name() { 206 | t.Fail() 207 | } 208 | 209 | //t.Fail() 210 | } 211 | 212 | type testPrimitives struct { 213 | Str string 214 | Int64 int64 215 | UInt64 uint64 216 | Bool bool 217 | Bool2 bool 218 | Bool3 bool 219 | Bool4 bool 220 | Float32 float32 221 | Float64 float64 222 | 223 | TimeType time.Time 224 | } 225 | 226 | func TestMustParse(t *testing.T) { 227 | out := testPrimitives{} 228 | f := testfile("str okay") 229 | defer rm(t, f) 230 | MustParse(&out, f, nil) 231 | 232 | defer func() { 233 | err := recover() 234 | if err == nil { 235 | t.Errorf("expected panic") 236 | } 237 | 238 | expected := " line 1: error parsing not: unknown option (field Not or Nots is missing)" 239 | if !strings.HasSuffix(err.(error).Error(), expected) { 240 | t.Errorf("\nexpected: %#v\nout: %#v\n", expected, err.(error).Error()) 241 | } 242 | }() 243 | 244 | f2 := testfile("not okay") 245 | defer rm(t, f2) 246 | MustParse(&out, f2, nil) 247 | } 248 | 249 | func TestParseError(t *testing.T) { 250 | out := testPrimitives{} 251 | err := Parse(&out, "/nonexistent-file", nil) 252 | if err == nil { 253 | t.Error("no error when parsing /nonexistent-file") 254 | } 255 | e := testPrimitives{} 256 | if out != e { 257 | t.Error("out isn't empty") 258 | } 259 | 260 | } 261 | 262 | // Make sure we give a sane error 263 | func TestGetValues(t *testing.T) { 264 | out := struct { 265 | Foo string 266 | }{} 267 | 268 | f := testfile(`foo bar`) 269 | defer rm(t, f) 270 | 271 | err := Parse(out, f, nil) 272 | if err == nil { 273 | t.Fatal("Err is nil") 274 | } 275 | switch err.(type) { 276 | case *reflect.ValueError: 277 | t.Fatal("still reflect.ValueError") 278 | } 279 | } 280 | 281 | func TestParsePrimitives(t *testing.T) { 282 | test := ` 283 | str foo bar 284 | int64 46 285 | uint64 51 286 | bool yes 287 | bool2 true 288 | bool3 289 | bool4 no 290 | float32 3.14 291 | float64 3.14159 292 | ` 293 | expected := testPrimitives{ 294 | Str: "foo bar", 295 | Int64: 46, 296 | UInt64: 51, 297 | Bool: true, 298 | Bool2: true, 299 | Bool3: true, 300 | Bool4: false, 301 | Float32: 3.14, 302 | Float64: 3.14159, 303 | } 304 | 305 | out := testPrimitives{} 306 | f := testfile(test) 307 | defer rm(t, f) 308 | err := Parse(&out, f, nil) 309 | if err != nil { 310 | t.Error(err.Error()) 311 | } 312 | if out != expected { 313 | t.Errorf("\nexpected: %#v\nout: %#v\n", expected, out) 314 | } 315 | } 316 | 317 | func TestInvalidPrimitives(t *testing.T) { 318 | tests := map[string]string{ 319 | "\n\nInt64 false": `line 3: error parsing Int64: strconv.ParseInt: parsing "false": invalid syntax`, 320 | "Bool what?": `line 1: error parsing Bool: unable to parse "what?" as a boolean`, 321 | "woot field": `line 1: error parsing woot: unknown option (field Woot or Woots is missing)`, 322 | "\n\n\n\ntime-type 2016\n\n": `line 5: error parsing time-type: don't know how to set fields of the type time.Time`, 323 | 324 | "float32 42,42": `invalid syntax`, 325 | "float64 42,42": `invalid syntax`, 326 | 327 | "int64 nope": `invalid syntax`, 328 | "uint64 nope": `invalid syntax`, 329 | 330 | `int64 1 2`: `line 1: error parsing int64: must have exactly one value`, 331 | `uint64`: `line 1: error parsing uint64: must have exactly one value`, 332 | } 333 | 334 | for test, expected := range tests { 335 | f := testfile(test) 336 | defer rm(t, f) 337 | 338 | out := testPrimitives{} 339 | err := Parse(&out, f, nil) 340 | if err == nil { 341 | t.Error("got to have an error") 342 | t.FailNow() 343 | } 344 | if !strings.HasSuffix(err.Error(), expected) { 345 | t.Errorf("\nexpected: %#v\nout: %#v\n", expected, err.Error()) 346 | } 347 | } 348 | } 349 | 350 | func TestDefaults(t *testing.T) { 351 | out := testPrimitives{ 352 | Str: "default value", 353 | } 354 | f := testfile("bool on\n") 355 | defer rm(t, f) 356 | err := Parse(&out, f, nil) 357 | if err != nil { 358 | t.Error(err.Error()) 359 | } 360 | if out.Str != "default value" { 361 | t.Error() 362 | } 363 | 364 | f2 := testfile("str changed\n") 365 | defer rm(t, f2) 366 | err = Parse(&out, f2, nil) 367 | if err != nil { 368 | t.Error(err.Error()) 369 | } 370 | if out.Str != "changed" { 371 | t.Error() 372 | } 373 | } 374 | 375 | func TestParseHandlers(t *testing.T) { 376 | out := testPrimitives{} 377 | f := testfile("bool false\nInt64 42\n") 378 | defer rm(t, f) 379 | 380 | err := Parse(&out, f, Handlers{ 381 | "Bool": func(line []string) (err error) { 382 | if line[0] == "false" { 383 | out.Bool = true 384 | } 385 | return 386 | }, 387 | }) 388 | if err != nil { 389 | t.Error(err.Error()) 390 | } 391 | if !out.Bool { 392 | t.Error() 393 | } 394 | 395 | err = Parse(&out, f, Handlers{ 396 | "Int64": func(line []string) (err error) { 397 | return errors.New("oh noes") 398 | }, 399 | }) 400 | if err == nil { 401 | t.Error("error is nil") 402 | } 403 | expected := " line 2: error parsing Int64: oh noes (from handler)" 404 | if !strings.HasSuffix(err.Error(), expected) { 405 | t.Errorf("\nexpected: %#v\nout: %#v\n", expected, err.Error()) 406 | } 407 | } 408 | 409 | type testArray struct { 410 | Str []string 411 | Int64 []int64 412 | UInt64 []uint64 413 | Bool []bool 414 | Float32 []float32 415 | Float64 []float64 416 | TimeType []time.Time 417 | } 418 | 419 | func TestParseArray(t *testing.T) { 420 | test := ` 421 | str foo bar 422 | str append this 423 | int64 46 700 424 | uint64 51 705 425 | bool yes no yes 426 | float32 3.14 1.1 427 | float64 3.14159 1.2 428 | ` 429 | 430 | expected := testArray{ 431 | Str: []string{"foo", "bar", "append", "this"}, 432 | Int64: []int64{46, 700}, 433 | UInt64: []uint64{51, 705}, 434 | Bool: []bool{true, false, true}, 435 | Float32: []float32{3.14, 1.1}, 436 | Float64: []float64{3.14159, 1.2}, 437 | } 438 | 439 | out := testArray{} 440 | f := testfile(test) 441 | defer rm(t, f) 442 | err := Parse(&out, f, nil) 443 | if err != nil { 444 | t.Error(err.Error()) 445 | } 446 | if fmt.Sprintf("%#v", out) != fmt.Sprintf("%#v", expected) { 447 | t.Errorf("\nexpected: %#v\nout: %#v\n", expected, out) 448 | } 449 | } 450 | 451 | func TestInvalidArray(t *testing.T) { 452 | tests := map[string]string{ 453 | "\n\nInt64 false": `line 3: error parsing Int64: strconv.ParseInt: parsing "false": invalid syntax`, 454 | "Bool what?": `line 1: error parsing Bool: unable to parse "what?" as a boolean`, 455 | "woot field": `line 1: error parsing woot: unknown option (field Woot or Woots is missing)`, 456 | "\n\n\n\ntime-type 2016\n\n": `line 5: error parsing time-type: don't know how to set fields of the type []time.Time`, 457 | 458 | "float32 42,42": `invalid syntax`, 459 | "float64 42,42": `invalid syntax`, 460 | 461 | "int64 nope": `invalid syntax`, 462 | "uint64 nope": `invalid syntax`, 463 | 464 | "int64": `line 1: error parsing int64: must have more than 1 values (has: 0)`, 465 | } 466 | 467 | for test, expected := range tests { 468 | f := testfile(test) 469 | defer rm(t, f) 470 | 471 | out := testArray{} 472 | err := Parse(&out, f, nil) 473 | if err == nil { 474 | t.Errorf("got to have an error for %v", test) 475 | t.FailNow() 476 | } 477 | if !strings.HasSuffix(err.Error(), expected) { 478 | t.Errorf("\nexpected: %#v\nout: %#v\n", expected, err.Error()) 479 | } 480 | } 481 | 482 | } 483 | 484 | func TestInflect(t *testing.T) { 485 | c := &struct { 486 | Key []string 487 | Planes []string 488 | }{} 489 | 490 | f := testfile("key a\nplanes b\nkeys a\nplane b") 491 | defer rm(t, f) 492 | 493 | err := Parse(c, f, nil) 494 | if err != nil { 495 | t.Fatal(err) 496 | } 497 | } 498 | 499 | // Make sure it doesn't panic. 500 | func TestWeirdType(t *testing.T) { 501 | f := testfile("foo.bar a\nasd.zxc 42\n") 502 | defer rm(t, f) 503 | 504 | c := "foo" 505 | err := Parse(&c, f, nil) 506 | if err == nil { 507 | t.Fatal("no err?!") 508 | } 509 | } 510 | 511 | func TestMapString(t *testing.T) { 512 | f := testfile("foo.bar a\nasd.zxc 42\n") 513 | defer rm(t, f) 514 | 515 | c := map[string][]string{} 516 | err := Parse(&c, f, nil) 517 | if err != nil { 518 | t.Fatal(err) 519 | } 520 | 521 | if !reflect.DeepEqual(c["foo.bar"], []string{"a"}) { 522 | t.Errorf("wrong output: %#v", c["foo.bar"]) 523 | } 524 | } 525 | 526 | func TestX(t *testing.T) { 527 | f := testfile("hello one two three\nhello foo bar") 528 | defer rm(t, f) 529 | 530 | c := struct { 531 | Hello []string 532 | }{} 533 | err := Parse(&c, f, Handlers{ 534 | "Hello": func(line []string) error { 535 | return nil 536 | }, 537 | }) 538 | if err != nil { 539 | t.Fatal(err) 540 | } 541 | } 542 | 543 | func TestFields(t *testing.T) { 544 | c := testPrimitives{Str: "init"} 545 | names := Fields(&c) 546 | 547 | v, ok := names["Str"] 548 | if !ok { 549 | t.Fatalf("Str not in map") 550 | } 551 | if v.Interface().(string) != "init" { 552 | t.Fatalf("Str wrong value") 553 | } 554 | 555 | v.SetString("XXX") 556 | if v.Interface().(string) != "XXX" { 557 | t.Fatalf("Str wrong value") 558 | } 559 | } 560 | 561 | type Marsh struct{ v string } 562 | 563 | func (m *Marsh) UnmarshalText(text []byte) error { 564 | m.v = string(text) 565 | if m.v == "error" { 566 | return errors.New("error") 567 | } 568 | return nil 569 | } 570 | 571 | func TestTextUnmarshaler(t *testing.T) { 572 | c := struct{ Field *Marsh }{} 573 | 574 | t.Run("set value", func(t *testing.T) { 575 | f := testfile("field !! ??") 576 | defer rm(t, f) 577 | 578 | err := Parse(&c, f, nil) 579 | if err != nil { 580 | t.Fatal("error", err) 581 | } 582 | if c.Field.v != "!! ??" { 583 | t.Errorf("value wrong: %#v", c.Field.v) 584 | } 585 | }) 586 | 587 | t.Run("error", func(t *testing.T) { 588 | f := testfile("field error") 589 | defer rm(t, f) 590 | 591 | err := Parse(&c, f, nil) 592 | if err == nil { 593 | t.Fatal("error is nil") 594 | } 595 | if !strings.Contains(err.Error(), "line 1: error parsing field: error") { 596 | t.Errorf("wrong error: %#v", err.Error()) 597 | } 598 | }) 599 | } 600 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package sconfig 2 | 3 | // This file contains some type handlers that can be used for validation. 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | // Errors used by the validation handlers. 11 | var ( 12 | errValidateNoValue = errors.New("does not accept any values") 13 | errValidateSingleValue = errors.New("must have exactly one value") 14 | errValidateValueLimitMore = "must have more than %v values (has: %v)" 15 | errValidateValueLimitFewer = "must have fewer than %v values (has: %v)" 16 | ) 17 | 18 | // ValidateNoValue returns a type handler that will return an error if there are 19 | // any values. 20 | func ValidateNoValue() TypeHandler { 21 | return func(v []string) (interface{}, error) { 22 | if len(v) != 0 { 23 | return nil, errValidateNoValue 24 | } 25 | return v, nil 26 | } 27 | } 28 | 29 | // ValidateSingleValue returns a type handler that will return an error if there 30 | // is more than one value or if there are no values. 31 | func ValidateSingleValue() TypeHandler { 32 | return func(v []string) (interface{}, error) { 33 | if len(v) != 1 { 34 | return nil, errValidateSingleValue 35 | } 36 | return v, nil 37 | } 38 | } 39 | 40 | // ValidateValueLimit returns a type handler that will return an error if there 41 | // either more values than max, or fewer values than min. 42 | func ValidateValueLimit(min, max int) TypeHandler { 43 | return func(v []string) (interface{}, error) { 44 | switch { 45 | case min > 0 && len(v) < min: 46 | return nil, fmt.Errorf(errValidateValueLimitMore, min, len(v)) 47 | case max > 0 && len(v) > max: 48 | return nil, fmt.Errorf(errValidateValueLimitFewer, max, len(v)) 49 | default: 50 | return v, nil 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /validate_test.go: -------------------------------------------------------------------------------- 1 | package sconfig 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestValidate(t *testing.T) { 10 | cases := []struct { 11 | fun TypeHandler 12 | in []string 13 | expectedErr error 14 | }{ 15 | {ValidateNoValue(), []string{}, nil}, 16 | {ValidateNoValue(), []string{"1"}, errValidateNoValue}, 17 | {ValidateNoValue(), []string{"asd", "zxa"}, errValidateNoValue}, 18 | 19 | {ValidateSingleValue(), []string{"qwe"}, nil}, 20 | {ValidateSingleValue(), []string{}, errValidateSingleValue}, 21 | {ValidateSingleValue(), []string{"asd", "zxc"}, errValidateSingleValue}, 22 | 23 | {ValidateValueLimit(0, 1), []string{}, nil}, 24 | {ValidateValueLimit(0, 1), []string{"Asd"}, nil}, 25 | {ValidateValueLimit(0, 1), []string{"zxc", "asd"}, fmt.Errorf(errValidateValueLimitFewer, 1, 2)}, 26 | 27 | {ValidateValueLimit(2, 3), []string{}, fmt.Errorf(errValidateValueLimitMore, 2, 0)}, 28 | {ValidateValueLimit(2, 3), []string{"ads"}, fmt.Errorf(errValidateValueLimitMore, 2, 1)}, 29 | {ValidateValueLimit(2, 3), []string{"ads", "asd"}, nil}, 30 | {ValidateValueLimit(2, 3), []string{"ads", "zxc", "qwe"}, nil}, 31 | {ValidateValueLimit(2, 3), []string{"ads", "zxc", "qwe", "hjkl"}, fmt.Errorf(errValidateValueLimitFewer, 3, 4)}, 32 | } 33 | 34 | for i, tc := range cases { 35 | t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { 36 | out, err := tc.fun(tc.in) 37 | 38 | switch tc.expectedErr { 39 | case nil: 40 | if err != nil { 41 | t.Errorf("expected err to be nil; is: %#v", err) 42 | } 43 | if !reflect.DeepEqual(out, tc.in) { 44 | t.Errorf("out wrong\nexpected: %#v\nout: %#v\n", 45 | tc.in, out) 46 | } 47 | default: 48 | if err.Error() != tc.expectedErr.Error() { 49 | t.Errorf("err wrong\nexpected: %#v\nout: %#v\n", 50 | tc.expectedErr, err) 51 | } 52 | 53 | if out != nil { 54 | t.Errorf("out should be nil if there's an error") 55 | } 56 | } 57 | 58 | }) 59 | } 60 | } 61 | --------------------------------------------------------------------------------