├── .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 |
--------------------------------------------------------------------------------