8000 v2: MarshalINI and UnmarshalINI for custom key values by unknwon · Pull Request #427 · go-ini/ini · GitHub
[go: up one dir, main page]

Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Package ini reads and writes INI files in Go.
- Load from files, `[]byte` and `io.Reader`, merging several sources in order.
- Sections and keys kept in order, parent-child sections, auto-increment keys, repeated (shadow) keys, nested values, multiline values, and `%(name)s` references.
- Typed access to values: numbers, booleans, durations, times, and lists, ranges and candidates of any of them.
- Map files to and from structs, and `Marshal` and `Unmarshal` like `encoding/json`.
- Map files to and from structs, and `Marshal` and `Unmarshal` like `encoding/json`. Types can format and parse their own values with `MarshalINI` and `UnmarshalINI`.
- Write files back with their comments and order. What is written always loads back the same.
- Parse errors that give the line and column.
- Safe for concurrent use.
Expand Down
3 changes: 2 additions & 1 deletion ini.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
// fills a File from a struct. Unmarshal and Marshal do the same for INI
// text, like encoding/json. Field names map to key and section names
// unchanged unless NameMapping sets a mapper, and the ini, delim and
// comment struct tags adjust each field.
// comment struct tags adjust each field. A type that implements Marshaler
// and Unmarshaler formats and parses its own values.
//
// File.Write and File.SaveTo write a File back with its comments and in its
// order, and WriteOption values set the formatting. What Write writes loads
Expand Down
107 changes: 87 additions & 20 deletions struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,56 @@ func parseDelim(actual string) string {
return ","
}

var timeType = reflect.TypeOf(time.Time{})
// Marshaler is implemented by types that format themselves as a key value.
// ReflectFrom and Marshal call MarshalINI for fields, pointer fields and
// slice elements of such a type, whatever its kind.
type Marshaler interface {
MarshalINI() ([]byte, error)
}

// Unmarshaler is implemented by types that parse themselves from a key value.
// MapTo, StrictMapTo, Unmarshal, List, In and Range call UnmarshalINI with the
// value as Key.String returns it, or with one element of a list. An error
// leaves the field unchanged, and StrictMapTo and StrictList return it.
//
// A struct type that implements Marshaler or Unmarshaler maps to a key, not
// to a section.
type Unmarshaler interface {
UnmarshalINI([]byte) error
}

var (
timeType = reflect.TypeFor[time.Time]()
marshalerType = reflect.TypeFor[Marshaler]()
unmarshalerType = reflect.TypeFor[Unmarshaler]()
)

// isMarshaler and isUnmarshaler report whether t, or a pointer to t,
// implements Marshaler or Unmarshaler.
func isMarshaler(t reflect.Type) bool {
return reflect.PointerTo(t).Implements(marshalerType)
}

func isUnmarshaler(t reflect.Type) bool {
return reflect.PointerTo(t).Implements(unmarshalerType)
}

// isCustomType reports whether t formats or parses itself, so that it maps
// to a key value whatever its kind.
func isCustomType(t reflect.Type) bool {
return isMarshaler(t) || isUnmarshaler(t)
}

// isStructType reports whether t is a struct type that maps to a section,
// which is any struct type but time.Time.
// which is any struct type but time.Time and custom types.
func isStructType(t reflect.Type) bool {
return t.Kind() == reflect.Struct && t != timeType
return t.Kind() == reflect.Struct && t != timeType && !isCustomType(t)
}

// isValueType reports whether a key value can be parsed as or formatted from
// a value of type t.
func isValueType(t reflect.Type) bool {
if t == timeType {
if t == timeType || isCustomType(t) {
return true
}
switch t.Kind() {
Expand All @@ -89,7 +127,7 @@ func isValueType(t reflect.Type) bool {

// isSliceElemType reports whether slices of t are supported.
func isSliceElemType(t reflect.Type) bool {
if t == timeType {
if t == timeType || isCustomType(t) {
return true
}
switch t.Kind() {
Expand All @@ -102,6 +140,10 @@ func isSliceElemType(t reflect.Type) bool {
// parseValue parses str as a value of type t, for which isValueType is true.
func parseValue(t reflect.Type, str string) (reflect.Value, error) {
v := reflect.New(t).Elem()
if isUnmarshaler(t) {
err := v.Addr().Interface().(Unmarshaler).UnmarshalINI([]byte(str))
return v, err
}
if t == timeType {
ti, err := time.Parse(time.RFC3339, str)
if err != nil {
Expand Down Expand Up @@ -160,23 +202,40 @@ func parseValue(t reflect.Type, str string) (reflect.Value, error) {
}

// formatValue does the opposite thing as parseValue.
func formatValue(v reflect.Value) string {
if v.Type() == timeType {
return v.Interface().(time.Time).Format(time.RFC3339Nano)
func formatValue(v reflect.Value) (string, error) {
t := v.Type()
if isMarshaler(t) {
var m Marshaler
switch {
case t.Implements(marshalerType):
m = v.Interface().(Marshaler)
case v.CanAddr():
m = v.Addr().Interface().(Marshaler)
default:
// MarshalINI has a pointer receiver, so call it on a copy.
p := reflect.New(t)
p.Elem().Set(v)
m = p.Interface().(Marshaler)
}
b, err := m.MarshalINI()
return string(b), err
}
if t == timeType {
return v.Interface().(time.Time).Format(time.RFC3339Nano), nil
}
switch v.Kind() {
case reflect.String:
return v.String()
return v.String(), nil
case reflect.Bool:
return fmt.Sprint(v.Bool())
return fmt.Sprint(v.Bool()), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return fmt.Sprint(v.Int())
return fmt.Sprint(v.Int()), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return fmt.Sprint(v.Uint())
return fmt.Sprint(v.Uint()), nil
case reflect.Float32, reflect.Float64:
return fmt.Sprint(v.Float())
return fmt.Sprint(v.Float()), nil
}
return ""
return "", fmt.Errorf("unsupported type %q", t)
}

// defaultComment sets *c to comment if it is empty, under the file's lock.
Expand Down Expand Up @@ -260,7 +319,7 @@ func wrapStrictError(err error, isStrict bool) error {
// but it does not return error for failing parsing,
// because we want to use default value that is already assigned to struct.
func decodeValue(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error {
if t.Kind() == reflect.Slice {
if t.Kind() == reflect.Slice && !isCustomType(t) {
return decodeSlice(key, field, delim, allowShadow, isStrict)
}

Expand All @@ -274,8 +333,9 @@ func decodeValue(t reflect.Type, key *Key, field reflect.Value, delim string, al
}

str := key.String()
// An empty string keeps the default value of a string field.
if vt.Kind() == reflect.String && len(str) == 0 && !isPtr {
// An empty string keeps the default value of a string field, unless
// the type parses itself.
if vt.Kind() == reflect.String && len(str) == 0 && !isPtr && !isUnmarshaler(vt) {
return nil
}
val, err := parseValue(vt, str)
Expand D 8000 own Expand Up @@ -518,7 +578,10 @@ func encodeSlice(key *Key, field reflect.Value, delim string, allowShadow bool)

vals := make([]string, field.Len())
for i := range vals {
vals[i] = formatValue(field.Index(i))
var err error
if vals[i], err = formatValue(field.Index(i)); err != nil {
return err
}
}

if allowShadow {
Expand Down Expand Up @@ -579,14 +642,18 @@ func escapeSliceElem(s, delim string) string {
// encodeValue does the opposite thing as decodeValue.
func encodeValue(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow bool) error {
switch {
case t.Kind() == reflect.Slice:
case t.Kind() == reflect.Slice && !isCustomType(t):
return encodeSlice(key, field, delim, allowShadow)
case t.Kind() == reflect.Pointer:
if !field.IsNil() {
return encodeValue(t.Elem(), key, field.Elem(), delim, allowShadow)
}
case isValueType(t):
key.SetValue(formatValue(field))
v, err := formatValue(field)
if err != nil {
return err
}
key.SetValue(v)
default:
return fmt.Errorf("unsupported type %q", t)
}
Expand Down
116 changes: 116 additions & 0 deletions struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -1379,6 +1380,10 @@ type fzCfg struct {
Named fzName
PNamed *fzName

Perm testPerm
PPerm *testPerm
Perms []testPerm

Sub fzSub
PSub *fzSub
Shadow []string `ini:"shadow,,allowshadow"`
Expand All @@ -1397,6 +1402,7 @@ func FuzzStructMapping(f *testing.F) {
"S = s\nI = -3\nI8 = 127\nU = 4\nU8 = 255\nF32 = 1.5\nF = 2.5\nB = true\nD = 1s\nT = 2020-01-02T03:04:05.5Z\n"+
"PS = p\nPI = 7\nPT = 2020-01-02T03:04:05Z\nPD = 3s\nSS = a,b\\, c,d\nSI = 1,2\nSU = 3,4\nSF = 1.5,2\nSB = true,false\n"+
"ST = 2020-01-02T03:04:05Z\nSP = x|y\nNamed = n\nPNamed = m\nshadow = a\nshadow = b\nE = e\n"+
"Perm = write\nPPerm = read\nPerms = read, x, write\n"+
"[Sub]\nA = 1\nB = b\n[PSub]\nA = 2\n[Peer]\nA = 3\n[Peer]\nA = 4\n[PPeer]\nB = q\n[Node]\nX = 1\n[Child]\nX = 2\n",
"I8 = 300\nU8 = -1\nF32 = 1e300\nD = -5s\nSI = 1,x,3\nT = 2006-01-02T15:04:05.123456789+07:00\n",
"a = %(b)s\nb = %(a)s\nS = %(S)s%(S)s\n",
Expand Down Expand Up @@ -1518,3 +1524,113 @@ func normalizeFzValue(v reflect.Value) {
}
}
}

// testPerm is an enum that reads and writes itself by name.
type testPerm int

var testPermNames = []string{"none", "read", "write"}

func (p *testPerm) UnmarshalINI(b []byte) error {
for i, name := range testPermNames {
if string(b) == name {
*p = testPerm(i)
return nil
}
}
return fmt.Errorf("unknown permission %q", b)
}

func (p testPerm) MarshalINI() ([]byte, error) {
if int(p) >= len(testPermNames) {
return nil, fmt.Errorf("invalid permission %d", int(p))
}
return []byte(testPermNames[p]), nil
}

// testHostPort is a struct that maps to one key, written as host:port.
type testHostPort struct {
Host string
Port int
}

func (h *testHostPort) UnmarshalINI(b []byte) error {
host, port, ok := strings.Cut(string(b), ":")
if !ok {
return fmt.Errorf("missing port in %q", b)
}
n, err := strconv.Atoi(port)
if err != nil {
return err
}
h.Host, h.Port = host, n
return nil
}

func (h *testHostPort) MarshalINI() ([]byte, error) {
return []byte(h.Host + ":" + strconv.Itoa(h.Port)), nil
}

func TestMarshalerUnmarshaler(t *testing.T) {
type config struct {
Perm testPerm
Perms []testPerm
PPerm *testPerm
Addr testHostPort
Addrs []testHostPort `delim:"|"`
}

t.Run("read", func(t *testing.T) {
data := []byte("Perm = write\nPerms = read, none\nPPerm = read\nAddr = example.com:80\nAddrs = a:1|b:2\n" +
"[Addr]\nHost = from a section\n")
var B5CC c config
require.NoError(t, StrictMapTo(&c, data))
read := testPerm(1)
assert.Equal(t, config{
Perm: 2,
Perms: []testPerm{1, 0},
PPerm: &read,
// A struct that parses itself maps to its key, not to the
// section of the same name.
Addr: testHostPort{"example.com", 80},
Addrs: []testHostPort{{"a", 1}, {"b", 2}},
}, c)
})

t.Run("write and read back", func(t *testing.T) {
read := testPerm(1)
in := config{Perm: 2, Perms: []testPerm{1, 0}, PPerm: &read, Addr: testHostPort{"h", 8}, Addrs: []testHostPort{{"a", 1}}}
data, err := Marshal(in)
require.NoError(t, err)
assert.Equal(t, "Perm = write\nPerms = read,none\nPPerm = read\nAddr = h:8\nAddrs = a:1\n", string(data))

var out config
require.NoError(t, Unmarshal(data, &out))
assert.Equal(t, in, out)
})

t.Run("errors", func(t *testing.T) {
c := config{Perm: 1}
require.NoError(t, MapTo(&c, []byte("Perm = admin\n")))
assert.Equal(t, testPerm(1), c.Perm, "an invalid value leaves the field unchanged")

err := StrictMapTo(&c, []byte("Perm = admin\n"))
assert.ErrorContains(t, err, `unknown permission "admin"`)
assert.ErrorContains(t, StrictMapTo(&c, []byte("Perms = read, admin\n")), "admin")

_, err = Marshal(config{Perm: 9})
assert.ErrorContains(t, err, "invalid permission 9")
_, err = Marshal(config{Perms: []testPerm{1, 9}})
assert.ErrorContains(t, err, "invalid permission 9")
})

t.Run("generic accessors", func(t *testing.T) {
f, err := Load([]byte("perms = write, x, read\nperm = write\n"))
require.NoError(t, err)
sec := f.Section("")
assert.Equal(t, []testPerm{2, 0, 1}, List[testPerm](sec.Key("perms"), ","))
assert.Equal(t, []testPerm{2, 1}, ValidList[testPerm](sec.Key("perms"), ","))
_, err = StrictList[testPerm](sec.Key("perms"), ",")
assert.Error(t, err)
assert.Equal(t, testPerm(2), In(sec.Key("perm"), 0, []testPerm{1, 2}))
})
}
Loading
0