Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/migrate-v1-to-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ port := sec.Key("PORT").MustInt(3000)
sec.Key("PORT").SetValue(strconv.Itoa(port))
```

### `Section.Key` no longer adds missing keys

In v1, `sec.Key("PORT")` added an empty `PORT` key when the section had none, so `HasKey`, `Keys` and `Write` saw it after a mere read. In v2 it returns a key that reads as empty, and only `SetValue` or `SetComment` adds it, or `AddShadow` and `AddNestedValue` when the file allows shadows or nested values. `sec.Key(name).SetValue(v)` works as before.

### `Reload` and `Append`

- `Reload` replaces the contents with what the sources hold now. v1 merged them in, so removed keys stayed and shadow values piled up. `*Section` and `*Key` values you hold see the new values. On error, nothing changes.
Expand Down
71 changes: 58 additions & 13 deletions key.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ type Key struct {
shadows []string
isAutoIncrement bool
isBooleanType bool
// missing is set on a key Section.Key returned for a name its section
// doesn't have. The key isn't in the section until a change adds it.
// If the section gains a key of its name first, it reads and changes
// that key instead, and is never added.
missing bool

nestedValues []string
}
Expand All @@ -36,6 +41,31 @@ func newKey(s *Section, name, val string) *Key {
}
}

// current returns the key k reads and changes. That is k, unless k is
// missing and its section has since gained a key of its name. The caller
// holds the lock.
func (k *Key) current() *Key {
if k.missing {
if key := k.s.byName[k.name]; key != nil {
return key
}
}
return k
}

// attach adds k to its section if it's missing and the section still has no
// key of its name, and returns the key to change: k, or the key of its name
// the section gained. The caller holds the write lock.
func (k *Key) attach() *Key {
c := k.current()
if c == k && k.missing {
k.missing = false
k.s.keys = append(k.s.keys, k)
k.s.byName[k.name] = k
}
return c
}

func (k *Key) addShadow(val string) error {
if k.isAutoIncrement || k.isBooleanType {
return errors.New("cannot add shadow to auto-increment or boolean key")
Expand All @@ -61,7 +91,7 @@ func (k *Key) AddShadow(val string) error {
}
k.s.f.maybeLock()
defer k.s.f.maybeUnlock()
return k.addShadow(val)
return k.attach().addShadow(val)
}

func (k *Key) addNestedValue(val string) error {
Expand All @@ -80,7 +110,7 @@ func (k *Key) AddNestedValue(val string) error {
}
k.s.f.maybeLock()
defer k.s.f.maybeUnlock()
return k.addNestedValue(val)
return k.attach().addNestedValue(val)
}

// ValueMapper represents a mapping function for values, e.g., os.ExpandEnv
Expand All @@ -96,27 +126,35 @@ func (k *Key) Name() string {
func (k *Key) Comment() string {
k.s.f.maybeRLock()
defer k.s.f.maybeRUnlock()
return k.comment
return k.current().comment
}

// SetComment sets the comment lines written before the key. Lines without a
// comment symbol get one.
func (k *Key) SetComment(comment string) {
k.s.f.maybeLock()
defer k.s.f.maybeUnlock()
k.comment = comment
k.attach().comment = comment
}

// Value returns raw value of key for performance purpose.
func (k *Key) Value() string {
_, v := k.currentValue()
return v
}

// currentValue returns the current key of k and its raw value.
func (k *Key) currentValue() (*Key, string) {
f := k.s.f
if !f.locking {
return k.value
c := k.current()
return c, c.value
}
f.mu.RLock()
v := k.value
c := k.current()
v := c.value
f.mu.RUnlock()
return v
return c, v
}

// Values returns raw values of key and its shadows if any. Empty
Expand All @@ -125,6 +163,7 @@ func (k *Key) Value() string {
func (k *Key) Values() []string {
k.s.f.maybeRLock()
defer k.s.f.maybeRUnlock()
k = k.current()
keepEmpty := k.s.f.options.PreserveEmptyShadowValues
vals := make([]string, 0, len(k.shadows)+1)
if keepEmpty || k.value != "" {
Expand All @@ -143,15 +182,15 @@ func (k *Key) Values() []string {
func (k *Key) IsBoolean() bool {
k.s.f.maybeRLock()
defer k.s.f.maybeRUnlock()
return k.isBooleanType
return k.current().isBooleanType
}

// NestedValues returns nested values stored in the key.
// It is possible returned value is nil if no nested values stored in the key.
func (k *Key) NestedValues() []string {
k.s.f.maybeRLock()
defer k.s.f.maybeRUnlock()
return k.nestedValues
return k.current().nestedValues
}

// expandValue takes a raw value and transforms to its final string.
Expand Down Expand Up @@ -215,7 +254,8 @@ func (k *Key) expand(val string, depth int, e *expansion) string {

// String returns string representation of value.
func (k *Key) String() string {
return k.expandValue(k.Value())
c, v := k.currentValue()
return c.expandValue(v)
}

// Validate accepts a validate function which can
Expand Down Expand Up @@ -484,7 +524,12 @@ func splitList(str, delim string) []string {
// StringsWithShadows returns list of string divided by given delimiter.
// Shadows will also be appended if any.
func (k *Key) StringsWithShadows(delim string) []string {
vals := k.Values()
// Expand with the key the values come from, so that a reference to its
// own name is looked up in the default section.
k.s.f.maybeRLock()
c := k.current()
k.s.f.maybeRUnlock()
vals := c.Values()
results := make([]string, 0, len(vals)*2)
for i := range vals {
if len(vals) == 0 {
Expand All @@ -495,7 +540,7 @@ func (k *Key) StringsWithShadows(delim string) []string {
}

for i := range results {
results[i] = k.expandValue(strings.TrimSpace(results[i]))
results[i] = c.expandValue(strings.TrimSpace(results[i]))
}
return results
}
Expand All @@ -504,7 +549,7 @@ func (k *Key) StringsWithShadows(delim string) []string {
func (k *Key) SetValue(v string) {
k.s.f.maybeLock()
defer k.s.f.maybeUnlock()
k.value = v
k.attach().value = v
}

// Scalar is a type a value can be parsed as, as struct mapping parses
Expand Down
22 changes: 15 additions & 7 deletions section.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,24 @@ func (s *Section) HasValue(value string) bool {
return false
}

// Key assumes named Key exists in section and returns a zero-value when not.
// Key returns the key with the given name, looking in parent sections too
// as GetKey does. When there is none, it returns a key that reads as empty
// without adding it to the section. SetValue or SetComment adds it, and so
// do AddShadow and AddNestedValue when the file allows shadows or nested
// values.
func (s *Section) Key(name string) *Key {
key, err := s.GetKey(name)
if err != nil {
if key, err = s.NewKey(name, ""); err != nil {
// An empty name can't be added, so return a key that isn't.
return newKey(s, name, "")
}
if key, err := s.GetKey(name); err == nil {
return key
}
key := newKey(s, name, "")
if name == "" {
// An empty name can't be added.
return key
}
if s.f.options.Insensitive || s.f.options.InsensitiveKeys {
key.name = strings.ToLower(name)
}
key.missing = true
return key
}

Expand Down
83 changes: 83 additions & 0 deletions section_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ini

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -187,6 +188,88 @@ func TestSection_Key(t *testing.T) {
assert.Equal(t, "18", k.Value())
})
})

t.Run("reading a missing key doesn't add it", func(t *testing.T) {
const data = "[app]\nname = demo\n\n[raw]\nbody\n"
f, err := Load([]byte(data), RawSections("raw"))
require.NoError(t, err)
app := f.Section("app")

k := app.Key("port")
assert.Equal(t, "port", k.Name())
assert.Equal(t, 8080, k.MustInt(8080))
assert.Empty(t, k.String())
assert.Empty(t, k.Values())
assert.Empty(t, k.Comment())
assert.Empty(t, k.NestedValues())
assert.False(t, k.IsBoolean())
_ = f.Section("raw").Key("x").String()

assert.False(t, app.HasKey("port"))
assert.Equal(t, []string{"name"}, app.KeyNames())
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
require.NoError(t, err, "a raw section read for a key stays writable")
assert.Equal(t, data, buf.String())
})

t.Run("changing a missing key adds it", func(t *testing.T) {
f, err := Load([]byte("[s]\n"), AllowShadows(true), AllowNestedValues(true))
require.NoError(t, err)
sec := f.Section("s")

sec.Key("a").SetValue("1")
sec.Key("b").SetComment("note")
require.NoError(t, sec.Key("c").AddShadow("x"))
require.NoError(t, sec.Key("d").AddNestedValue("n = 1"))
assert.Equal(t, []string{"a", "b", "c", "d"}, sec.KeyNames())
assert.Equal(t, "1", sec.Key("a").String())

k := sec.Key("e")
k.SetValue("2")
k.SetValue("3")
assert.Same(t, k, sec.Key("e"), "the key added is the one returned")
assert.Equal(t, []string{"a", "b", "c", "d", "e"}, sec.KeyNames())
})

t.Run("a missing key follows the key its section gains", func(t *testing.T) {
f, err := Load([]byte("[s]\n"), CaseInsensitiveKeys(true))
require.NoError(t, err)
sec := f.Section("s")

k1, k2 := sec.Key("Port"), sec.Key("port")
assert.Equal(t, "port", k1.Name())
k1.SetValue("1")
assert.Equal(t, "1", k2.String())
k2.SetValue("2")
assert.Equal(t, "2", k1.String())
assert.Equal(t, []string{"port"}, sec.KeyNames())

k3 := sec.Key("host")
_, err = sec.NewKey("host", "example.com")
require.NoError(t, err)
assert.Equal(t, "example.com", k3.String())
k3.SetValue("localhost")
assert.Equal(t, "localhost", sec.Key("host").String())
assert.Equal(t, []string{"port", "host"}, sec.KeyNames())
})

t.Run("a missing key expands as the key it follows", func(t *testing.T) {
f, err := Load([]byte("path = /usr\n[s]\n"))
require.NoError(t, err)
sec := f.Section("s")
k := sec.Key("path")
_, err = sec.NewKey("path", "%(path)s/bin")
require.NoError(t, err)
assert.Equal(t, "/usr/bin", k.String())
assert.Equal(t, []string{"/usr/bin"}, k.StringsWithShadows(","))
})

t.Run("an empty name is never added", func(t *testing.T) {
f := Empty()
f.Section("").Key("").SetValue("v")
assert.Empty(t, f.Section("").KeyNames())
})
}

func TestSection_Keys(t *testing.T) {
Expand Down
Loading