diff --git a/README.md b/README.md index 76a730c..7cb8331 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A small configuration library for go application with a viper-like API, but with It supports: - * reading a config in JSON format + * reading a config in JSON or JSONC ( JSON with comments) * setting default diff --git a/parser.go b/parser.go index fece6db..405d72f 100644 --- a/parser.go +++ b/parser.go @@ -1,6 +1,7 @@ package boa import ( + "bytes" "encoding/json" "fmt" "io" @@ -15,15 +16,27 @@ var ( ) // ParseConfig reads the config from an io.Reader. -func ParseConfig(jsonConfig io.Reader) error { +// +// The config may be in JSON or in JSONC ( json with comment) +// Allowed format for comments are +// * single line ( //... ) +// * multiligne ( /*...*/ ) +func ParseConfig(jsoncConfig io.Reader) error { + + jsonc, err := io.ReadAll(jsoncConfig) + if err != nil { + return fmt.Errorf("fail to read from reader: %v", err) + } + + cleanJson := removeComment(jsonc) - d := json.NewDecoder(jsonConfig) + d := json.NewDecoder(bytes.NewReader(cleanJson)) d.UseNumber() var src map[string]any - err := d.Decode(&src) + err = d.Decode(&src) if err != nil { - return err + return fmt.Errorf("fail to parse JSON: %v", err) } flatten("", src, config) @@ -31,6 +44,51 @@ func ParseConfig(jsonConfig io.Reader) error { return nil } +func removeComment(src []byte) []byte { + + output := make([]byte, 0, len(src)) + + var prev byte + inString := false + inSingleLineComment := false + inMultiligneComment := false + + for _, b := range src { + + switch b { + + case '"': + inString = !inString + case '/': + if !inString && prev == '/' { + output = output[0 : len(output)-1] + inSingleLineComment = true + } + + if inMultiligneComment && prev == '*' { + inMultiligneComment = false + continue + } + + case '*': + if !inString && prev == '/' { + output = output[0 : len(output)-1] + inMultiligneComment = true + } + case '\n': + inSingleLineComment = false + } + + prev = b + + if !inSingleLineComment && !inMultiligneComment { + output = append(output, b) + } + } + + return output +} + func flatten(prefix string, src map[string]any, dst map[string]any) { if len(prefix) > 0 { diff --git a/parser_test.go b/parser_test.go index 399dc5a..0050ddd 100644 --- a/parser_test.go +++ b/parser_test.go @@ -308,6 +308,65 @@ func TestSetDefaults(t *testing.T) { } } +func TestRemoveComment(t *testing.T) { + + config := ` + { + /* some + multiline + comment */ + "smtp": { + // single line + "enabled": true, // with trailing space + "host": "http://127.0.0.1",// without trailing space + "port": 55, + "pwd": "fhd/*|,;,bdo*/" + } /**/ + + + } + // ` + + loadConfig(t, config) + + tests := []struct { + name string + want any + got any + }{ + { + name: "smtp.enabled", + want: true, + got: boa.GetBool("smtp.enabled"), + }, + { + name: "smtp.host", + want: "http://127.0.0.1", + got: boa.GetString("smtp.host"), + }, + { + name: "smtp.port", + want: 55, + got: boa.GetInt("smtp.port"), + }, + { + name: "smtp.pwd", + want: "fhd/*|,;,bdo*/", + got: boa.GetString("smtp.pwd"), + }, + } + + for _, test := range tests { + + tt := test + t.Run(tt.name, func(t *testing.T) { + if tt.want != tt.got { + t.Errorf("expected '%v' but got '%v'", tt.want, tt.got) + } + }) + } +} + func loadConfig(t *testing.T, config string) { err := boa.ParseConfig(strings.NewReader(config)) if err != nil {