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
3 changes: 3 additions & 0 deletions docs/migrate-v1-to-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ In v1, `f.Section("server").Key("PORT")` added an empty `[server]` section and `
- Nil pointer fields are skipped.
- Fields after an empty `nonunique` slice or a `StructReflector` field are written.
- An `allowshadow` element containing the delimiter is an error, since it would read back as two elements.
- In a file loaded without `AllowShadows`, an `allowshadow` slice of several elements is written as one value joined by the delimiter, which reads back as all of them. v1 wrote only the first element.

A struct field maps to a section. When a file has a key of the field's name but no such section, `MapTo` leaves the field unchanged, as v1 did, and `StrictMapTo` returns an error. To parse that key yourself, tag the field `ini:"-"` so both skip it, then read it with `Section.Key`. To have `MapTo` and `ReflectFrom` map the key, give the field's type both `MarshalINI` and `UnmarshalINI`.

### Writing

Expand Down
12 changes: 10 additions & 2 deletions struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,9 @@ func wrapStrictError(err error, isStrict bool) error {

// decodeValue sets field from the value of key. A value that doesn't parse
// leaves the field as it is, so it keeps its default, and is an error only
// when isStrict. An unsupported type is always an error.
// when isStrict. So does a key for a struct field, which maps to a section,
// as when the program parses that key itself. Any other unsupported type is
// always an error.
func decodeValue(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error {
if t.Kind() == reflect.Slice && !isCustomType(t) {
return decodeSlice(key, field, delim, allowShadow, isStrict)
Expand All @@ -353,6 +355,9 @@ func decodeValue(t reflect.Type, key *Key, field reflect.Value, delim string, al
vt = t.Elem()
}
if !isValueType(vt) {
if isStructType(vt) {
return wrapStrictError(fmt.Errorf("type %q maps to a section, not a key", t), isStrict)
}
return fmt.Errorf("unsupported type %q", t)
}

Expand Down Expand Up @@ -615,8 +620,11 @@ func encodeSlice(key *Key, field reflect.Value, delim string, allowShadow bool)
return fmt.Errorf("element %q contains the delimiter %q, which an allowshadow slice can't hold", v, delim)
}
}
// Without AllowShadows the elements share one value, which the
// reader splits at delim all the same.
if len(vals) > 1 && !key.s.f.options.AllowShadows {
return errors.New("shadow key is not allowed")
key.SetValue(strings.Join(vals, delim))
return nil
}
// Shadows deduplicated as the file's options say, on a key that
// isn't boolean or auto-increment.
Expand Down
39 changes: 32 additions & 7 deletions struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1041,13 +1041,24 @@ func TestMapTo_DurationPointerWithNumber(t *testing.T) {
}

func TestMapTo_StructFieldsFromKeys(t *testing.T) {
// A struct field maps to a section. A key of its name, which a program
// such as frp parses itself, leaves the field as it is, and the fields
// around it still map, as in v1. StrictMapTo reports it.
type sub struct{ A int }
var c1 struct{ Sub sub }
assert.Error(t, MapTo(&c1, []byte("Sub = 2006-01-02T15:04:05Z\n")))
var c2 struct{ Sub *sub }
assert.Error(t, MapTo(&c2, []byte("Sub = 2006-01-02T15:04:05Z\n")))
var c3 struct{ Items []sub }
assert.Error(t, MapTo(&c3, []byte("Items = x\n")))
type cfg struct {
Port int
Sub sub
Ptr *sub
}
data := []byte("Port = 22\nSub = 1MB\nPtr = 1MB\n")
c := cfg{Sub: sub{A: 7}}
require.NoError(t, MapTo(&c, data))
assert.Equal(t, cfg{Port: 22, Sub: sub{A: 7}}, c)
assert.Error(t, StrictMapTo(&cfg{}, []byte("Sub = 1MB\n")))
assert.Error(t, StrictMapTo(&cfg{}, []byte("Ptr = 1MB\n")))

var items struct{ Items []sub }
assert.Error(t, MapTo(&items, []byte("Items = x\n")))
}

type testTag string
Expand Down Expand Up @@ -1319,8 +1330,22 @@ func TestReflectFrom_AllowShadow(t *testing.T) {
// The comment of a key goes before its last line, as the lexer reads it.
assert.Equal(t, "path = a\n; search paths\npath = b\n", string(data))

assert.Error(t, Empty().ReflectFrom(&cfg{Paths: []string{"a", "b"}}), "values must not be dropped silently")
// Without AllowShadows the elements share one value, which reads back
// as all of them with or without AllowShadows.
f := Empty()
require.NoError(t, f.ReflectFrom(&cfg{Paths: []string{"a", "b", "c"}}))
var buf bytes.Buffer
_, err = f.Write(&buf)
require.NoError(t, err)
assert.Equal(t, "; search paths\npath = a,b,c\n", buf.String())
for _, shadows := range []bool{false, true} {
var out cfg
require.NoError(t, MapTo(&out, buf.Bytes(), AllowShadows(shadows)))
assert.Equal(t, []string{"a", "b", "c"}, out.Paths, "AllowShadows(%v)", shadows)
}
assert.Error(t, Empty().ReflectFrom(&cfg{Paths: []string{"a", "b,c"}}))

f = Empty()
require.NoError(t, f.ReflectFrom(&cfg{Paths: []string{"a"}}))
assert.Equal(t, "a", f.Section("").Key("path").String())
}
Expand Down
Loading