diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4f2ee4d --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: go diff --git a/all_test.go b/all_test.go index 6441a6d..70567d3 100644 --- a/all_test.go +++ b/all_test.go @@ -26,10 +26,12 @@ const ( tmp = "testdata/__test.go" source = "testdata/source.cfg" target = "testdata/target.cfg" + muti = "testdata/muti.cfg" ) func testGet(t *testing.T, c *Config, section string, option string, expected interface{}) { + ok := false switch expected.(type) { case string: @@ -56,6 +58,60 @@ func testGet(t *testing.T, c *Config, section string, option string, } } +func testGetMuti(t *testing.T, c *Config, section string, option string, + expected interface{}) { + ok := false + switch expected.(type) { + case []string: + v, err := c.StringMuti(section, option) + if nil != err { + t.Fatalf("fail of err:%s", err) + } + if len(v) == len(expected.([]string)) { + ok = true + for index, expectedEle := range expected.([]string) { + if expectedEle != v[index] { + ok = false + } + } + + } + case []int: + v, err := c.IntMuti(section, option) + if nil != err { + t.Fatalf("fail of err:%s", err) + } + if len(v) == len(expected.([]int)) { + ok = true + for index, expectedEle := range expected.([]int) { + if expectedEle != v[index] { + ok = false + } + } + } + case []bool: + v, err := c.BoolMuti(section, option) + if nil != err { + t.Fatalf("fail of err:%s", err) + } + if len(v) == len(expected.([]bool)) { + ok = true + for index, expectedEle := range expected.([]bool) { + if expectedEle != v[index] { + ok = false + } + } + } + + default: + t.Fatalf("Bad test case:section:%s,option:%s", section, option) + } + if !ok { + v, _ := c.StringMuti(section, option) + t.Errorf("GetMuti failure: expected different value for %s %s (expected: [%#v] got: [%#v])", section, option, expected, v) + } +} + // TestInMemory creates configuration representation and run multiple tests in-memory. func TestInMemory(t *testing.T) { c := NewDefault() @@ -288,6 +344,41 @@ func TestWriteReadFile(t *testing.T) { defer os.Remove(tmp) } +// TestWriteReadFileMuti tests writing and reading back a configuration file. +func TestWriteReadFileMuti(t *testing.T) { + cw := NewDefault() + + // write file; will test only read later on + cw.AddSection("First-Section") + cw.AddOption("First-Section", MUTI_KEY_IDENTIFIER+"option1", "value option1") + cw.AddOption("First-Section", MUTI_KEY_IDENTIFIER+"option1", "value option2") + cw.AddOption("First-Section", "option2", "2") + + cw.AddOption("", "host", "www.example.com") + cw.AddOption(DEFAULT_SECTION, "protocol", "https://") + cw.AddOption(DEFAULT_SECTION, "base-url", "%(protocol)s%(host)s") + + cw.AddOption("Another-Section", MUTI_KEY_IDENTIFIER+"useHTTPS", "y") + cw.AddOption("Another-Section", MUTI_KEY_IDENTIFIER+"useHTTPS", "n") + cw.AddOption("Another-Section", MUTI_KEY_IDENTIFIER+"useHTTPS", "y") + cw.AddOption("Another-Section", "url", "%(base-url)s/some/path") + + cw.WriteFile(tmp, 0644, "Test file for test-case") + + // read back file and test + cr, err := ReadDefault(tmp) + if err != nil { + t.Fatalf("ReadDefault failure: %s", err) + } + + testGetMuti(t, cr, "First-Section", "option1", []string{"value option1", "value option2"}) + testGet(t, cr, "First-Section", "option2", 2) + testGetMuti(t, cr, "Another-Section", "useHTTPS", []bool{true, false, true}) + testGet(t, cr, "Another-Section", "url", "https://www.example.com/some/path") + + //defer os.Remove(tmp) +} + // TestSectionOptions tests read options in a section without default options. func TestSectionOptions(t *testing.T) { cw := NewDefault() @@ -360,41 +451,67 @@ func TestSectionOptions(t *testing.T) { defer os.Remove(tmp) } -// TestMerge tests merging 2 configurations. -func TestMerge(t *testing.T) { - target, error := ReadDefault(target) - if error != nil { - t.Fatalf("Unable to read target config file '%s'", target) +// TestReadFileMuti creates a 'tough' configuration file and test (read) parsing. +func TestReadFileMuti(t *testing.T) { + file, err := os.Create(muti) + if err != nil { + t.Fatal("Test cannot run because cannot write temporary file: " + muti) } - source, error := ReadDefault(source) - if error != nil { - t.Fatalf("Unable to read source config file '%s'", source) + err = os.Setenv("GO_CONFIGFILE_TEST_ENV_VAR", "configvalue12345") + if err != nil { + t.Fatalf("Test cannot run because cannot set environment variable GO_CONFIGFILE_TEST_ENV_VAR: %#v", err) } - target.Merge(source) + buf := bufio.NewWriter(file) + buf.WriteString("@optionInDefaultSection=true\n") + buf.WriteString("@optionInDefaultSection=false\n") + buf.WriteString("[section-1]\n") + buf.WriteString(" @option1=value1.1 ; This is a comment\n") + buf.WriteString(" @option1=value1.2 ; This is a comment\n") + buf.WriteString(" @option2 : 2.1#Not a comment\t#Now this is a comment after a TAB\n") + buf.WriteString(" @option2 : 2.2#Not a comment\t#Now this is a comment after a TAB\n") + buf.WriteString(" # Let me put another comment\n") + buf.WriteString("; Another comment\n") + buf.WriteString("@option5=on\n") + buf.WriteString("@option5=false\n") + buf.WriteString("@option5=false\n") + buf.WriteString("@option6=985\n") + buf.WriteString("@option6=211\n") + buf.WriteString("[" + DEFAULT_SECTION + "]\n") + buf.WriteString("variable1=small\n") + buf.WriteString("variable2=a_part_of_a_%(variable1)_test\n") + buf.WriteString("[secTION-2]\n") + buf.WriteString("@IS-flag-TRUE=Yes\n") + buf.WriteString("@IS-flag-TRUE=No\n") + buf.WriteString("[section-1]\n") // continue again [section-1] + buf.WriteString("option4=this_is_%(variable2)s.\n") + buf.WriteString("envoption1=this_uses_${GO_CONFIGFILE_TEST_ENV_VAR}_env\n") + buf.WriteString("@optionInDefaultSection=false\n") + buf.Flush() + file.Close() - // Assert whether a regular option was merged from source -> target - if result, _ := target.String(DEFAULT_SECTION, "one"); result != "source1" { - t.Errorf("Expected 'one' to be '1' but instead it was '%s'", result) - } - // Assert that a non-existent option in source was not overwritten - if result, _ := target.String(DEFAULT_SECTION, "five"); result != "5" { - t.Errorf("Expected 'five' to be '5' but instead it was '%s'", result) - } - // Assert that a folded option was correctly unfolded - if result, _ := target.String(DEFAULT_SECTION, "two_+_three"); result != "source2 + source3" { - t.Errorf("Expected 'two_+_three' to be 'source2 + source3' but instead it was '%s'", result) - } - if result, _ := target.String(DEFAULT_SECTION, "four"); result != "4" { - t.Errorf("Expected 'four' to be '4' but instead it was '%s'", result) + c, err := ReadDefault(muti) + if err != nil { + t.Fatalf("ReadDefault failure: %s", err) } - // Assert that a section option has been merged - if result, _ := target.String("X", "x.one"); result != "sourcex1" { - t.Errorf("Expected '[X] x.one' to be 'sourcex1' but instead it was '%s'", result) + // check number of sections + if len(c.Sections()) != 3 { + t.Errorf("Sections failure: wrong number of sections") } - if result, _ := target.String("X", "x.four"); result != "x4" { - t.Errorf("Expected '[X] x.four' to be 'x4' but instead it was '%s'", result) + + // check number of options 6 of [section-1] plus 2 of [default] + opts, err := c.Options("section-1") + if len(opts) != 9 { + t.Errorf("Options failure: wrong number of options: %d", len(opts)) } + + testGetMuti(t, c, "section-1", "option1", []string{"value1.1", "value1.2"}) + testGetMuti(t, c, "section-1", "option2", []string{"2.1#Not a comment", "2.2#Not a comment"}) + testGetMuti(t, c, "section-1", "option5", []bool{true, false, false}) + testGetMuti(t, c, "section-1", "option6", []int{985, 211}) + testGetMuti(t, c, "section-1", "optionInDefaultSection", []bool{false}) + testGetMuti(t, c, "section-2", "optionInDefaultSection", []bool{true, false}) + testGetMuti(t, c, "secTION-2", "IS-flag-TRUE", []bool{true, false}) // case-sensitive } diff --git a/config.go b/config.go index b7c17f4..7f09d13 100644 --- a/config.go +++ b/config.go @@ -29,6 +29,7 @@ const ( ALTERNATIVE_COMMENT = "; " DEFAULT_SEPARATOR = ":" ALTERNATIVE_SEPARATOR = "=" + MUTI_KEY_IDENTIFIER = "@" ) var ( @@ -70,8 +71,9 @@ type Config struct { // tValue holds the input position for a value. type tValue struct { - position int // Option order - v string // value + position int // Option order + v string // value + vMuti []string //value for muti key } // New creates an empty configuration representation. diff --git a/option.go b/option.go index c3184ed..4c37e6b 100644 --- a/option.go +++ b/option.go @@ -24,6 +24,7 @@ import "errors" // It returns true if the option and value were inserted, and false if the value // was overwritten. func (c *Config) AddOption(section string, option string, value string) bool { + c.AddSection(section) // Make sure section exists if section == "" { @@ -32,9 +33,24 @@ func (c *Config) AddOption(section string, option string, value string) bool { _, ok := c.data[section][option] - c.data[section][option] = &tValue{c.lastIdOption[section], value} + // Add muti key process + // muti key will start with MUTI_KEY_IDENTIFIER and may appear more than once + // key will be key except MUTI_KEY_IDENTIFIER and value will be an array + if MUTI_KEY_IDENTIFIER == string(option[0]) { + option = string(option[1:]) + var vMuti []string + val, ok := c.data[section][option] + if true == ok { + vMuti = val.vMuti + } else { + vMuti = []string{} + } + vMuti = append(vMuti, value) + c.data[section][option] = &tValue{c.lastIdOption[section], value, vMuti} + } else { + c.data[section][option] = &tValue{c.lastIdOption[section], value, []string{}} + } c.lastIdOption[section]++ - return !ok } diff --git a/read.go b/read.go index e287c75..614e14b 100644 --- a/read.go +++ b/read.go @@ -60,10 +60,12 @@ func (c *Config) read(buf *bufio.Reader) (err error) { for { l, err := buf.ReadString('\n') // parse line-by-line - if err == io.EOF { - break - } else if err != nil { - return err + if len(strings.TrimSpace(l)) == 0 { + if err == io.EOF { + break + } else if err != nil { + return err + } } l = strings.TrimSpace(l) @@ -97,6 +99,10 @@ func (c *Config) read(buf *bufio.Reader) (err error) { c.AddOption(section, option, value) // Continuation of multi-line value case section != "" && option != "": + + if MUTI_KEY_IDENTIFIER == string(option[0]) { + return errors.New("muti key not support muti line") + } prev, _ := c.RawString(section, option) value := strings.TrimSpace(stripComments(l)) c.AddOption(section, option, prev+"\n"+value) diff --git a/testdata/muti.cfg b/testdata/muti.cfg new file mode 100644 index 0000000..e37c0c5 --- /dev/null +++ b/testdata/muti.cfg @@ -0,0 +1,24 @@ +@optionInDefaultSection=true +@optionInDefaultSection=false +[section-1] + @option1=value1.1 ; This is a comment + @option1=value1.2 ; This is a comment + @option2 : 2.1#Not a comment #Now this is a comment after a TAB + @option2 : 2.2#Not a comment #Now this is a comment after a TAB + # Let me put another comment +; Another comment +@option5=on +@option5=false +@option5=false +@option6=985 +@option6=211 +[DEFAULT] +variable1=small +variable2=a_part_of_a_%(variable1)_test +[secTION-2] +@IS-flag-TRUE=Yes +@IS-flag-TRUE=No +[section-1] +option4=this_is_%(variable2)s. +envoption1=this_uses_${GO_CONFIGFILE_TEST_ENV_VAR}_env +@optionInDefaultSection=false diff --git a/type.go b/type.go index 304fc86..438eaca 100644 --- a/type.go +++ b/type.go @@ -77,6 +77,27 @@ func (c *Config) Bool(section string, option string) (value bool, err error) { return value, nil } +// BoolMuti has the same behaviour as String but converts the response to bool. +// See "boolString" for string values converted to bool. +func (c *Config) BoolMuti(section string, option string) (retValue []bool, err error) { + retValue = []bool{} + sv, err := c.StringMuti(section, option) + if err != nil { + return retValue, err + } + + for i := 0; i < len(sv); i++ { + value, ok := boolString[strings.ToLower(sv[i])] + if !ok { + return retValue, errors.New("could not parse bool value: " + sv[i]) + } + + retValue = append(retValue, value) + } + + return retValue, nil +} + // Float has the same behaviour as String but converts the response to float. func (c *Config) Float(section string, option string) (value float64, err error) { sv, err := c.String(section, option) @@ -87,6 +108,23 @@ func (c *Config) Float(section string, option string) (value float64, err error) return value, err } +// FloatMuti has the same behaviour as String but converts the response to float. +func (c *Config) FloatMuti(section string, option string) (retValue []float64, err error) { + retValue = []float64{} + sv, err := c.StringMuti(section, option) + for i := 0; i < len(sv); i++ { + var value float64 + if err == nil { + value, err = strconv.ParseFloat(sv[i], 64) + } else { + value = 0 + } + retValue = append(retValue, value) + } + + return retValue, err +} + // Int has the same behaviour as String but converts the response to int. func (c *Config) Int(section string, option string) (value int, err error) { sv, err := c.String(section, option) @@ -97,6 +135,23 @@ func (c *Config) Int(section string, option string) (value int, err error) { return value, err } +// Int has the same behaviour as String but converts the response to int. +func (c *Config) IntMuti(section string, option string) (retValue []int, err error) { + retValue = []int{} + sv, err := c.StringMuti(section, option) + for i := 0; i < len(sv); i++ { + var value int + if err == nil { + value, err = strconv.Atoi(sv[i]) + } else { + value = 0 + } + retValue = append(retValue, value) + } + + return retValue, err +} + // RawString gets the (raw) string value for the given option in the section. // The raw string value is not subjected to unfolding, which was illustrated in // the beginning of this documentation. @@ -111,6 +166,20 @@ func (c *Config) RawString(section string, option string) (value string, err err return c.RawStringDefault(option) } +// RawStringMuti gets the (raw) string value for the given option in the section. +// The raw string value is not subjected to unfolding, which was illustrated in +// the beginning of this documentation. +// +// It returns an error if either the section or the option do not exist. +func (c *Config) RawStringMuti(section string, option string) (value []string, err error) { + if _, ok := c.data[section]; ok { + if tValue, ok := c.data[section][option]; ok { + return tValue.vMuti, nil + } + } + return c.RawStringMutiDefault(option) +} + // RawStringDefault gets the (raw) string value for the given option from the // DEFAULT section. // @@ -122,6 +191,17 @@ func (c *Config) RawStringDefault(option string) (value string, err error) { return "", OptionError(option) } +// RawStringMutiDefault gets the (raw) string value for the given option from the +// DEFAULT section. +// +// It returns an error if the option does not exist in the DEFAULT section. +func (c *Config) RawStringMutiDefault(option string) (value []string, err error) { + if tValue, ok := c.data[DEFAULT_SECTION][option]; ok { + return tValue.vMuti, nil + } + return []string{}, OptionError(option) +} + // String gets the string value for the given option in the section. // If the value needs to be unfolded (see e.g. %(host)s example in the beginning // of this documentation), then String does this unfolding automatically, up to @@ -158,3 +238,47 @@ func (c *Config) String(section string, option string) (value string, err error) value = *computedVal return value, err } + +// StringMuti gets the string value for the given option in the section. +// If the value needs to be unfolded (see e.g. %(host)s example in the beginning +// of this documentation), then String does this unfolding automatically, up to +// _DEPTH_VALUES number of iterations. +// +// It returns an error if either the section or the option do not exist, or the +// unfolding cycled. +func (c *Config) StringMuti(section string, option string) (retValue []string, err error) { + retValue = []string{} + values, err := c.RawStringMuti(section, option) + if err != nil { + return []string{}, err + } + + for i := 0; i < len(values); i++ { + + value := values[i] + // % variables + computedVal, err := c.computeVar(&value, varRegExp, 2, 2, func(varName *string) string { + lowerVar := *varName + // search variable in default section as well as current section + varVal, _ := c.data[DEFAULT_SECTION][lowerVar] + if _, ok := c.data[section][lowerVar]; ok { + varVal = c.data[section][lowerVar] + } + return varVal.v + }) + value = *computedVal + + if err != nil { + return retValue, err + } + + // $ environment variables + computedVal, err = c.computeVar(&value, envVarRegExp, 2, 1, func(varName *string) string { + return os.Getenv(*varName) + }) + value = *computedVal + + retValue = append(retValue, value) + } + return retValue, err +} diff --git a/write.go b/write.go index 68cc8f5..aa7d159 100644 --- a/write.go +++ b/write.go @@ -69,10 +69,21 @@ func (c *Config) write(buf *bufio.Writer, header string) (err error) { for option, tValue := range sectionMap { if tValue.position == i { - if _, err = buf.WriteString(fmt.Sprint( - option, c.separator, tValue.v, "\n")); err != nil { - return err + + if len(tValue.vMuti) > 0 { + for _, realValue := range tValue.vMuti { + if _, err = buf.WriteString(fmt.Sprint( + "@", option, c.separator, realValue, "\n")); err != nil { + return err + } + } + } else { + if _, err = buf.WriteString(fmt.Sprint( + option, c.separator, tValue.v, "\n")); err != nil { + return err + } } + c.RemoveOption(section, option) break }