diff options
| author | Shulhan <ms@kilabit.info> | 2022-07-23 16:02:04 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2022-07-23 16:02:04 +0700 |
| commit | f6eeae8e1a4ae18c2e825db076b74531b4a343b6 (patch) | |
| tree | 196e79f140f5031b4a44c412b8970eaf6496ebbc | |
| parent | 95ccd72bc3d122847684ca7e3c213d909a10591c (diff) | |
| download | pakakeh.go-f6eeae8e1a4ae18c2e825db076b74531b4a343b6.tar.xz | |
lib/ini: support escaped double-quote and colon in tag subsection
A colon `:` is escaped using double backslash `\\`, for example
`a:b\\:c:d` contains section `a`, subsection `b:c`, and variable `d`.
A double quote `"` is escaped using triple backslash, for example `\\\"`.
| -rw-r--r-- | lib/ini/common.go | 96 | ||||
| -rw-r--r-- | lib/ini/common_test.go | 73 | ||||
| -rw-r--r-- | lib/ini/doc.go | 15 | ||||
| -rw-r--r-- | lib/ini/ini.go | 22 | ||||
| -rw-r--r-- | lib/ini/ini_test.go | 9 | ||||
| -rw-r--r-- | lib/ini/tag_struct_field.go | 20 |
6 files changed, 198 insertions, 37 deletions
diff --git a/lib/ini/common.go b/lib/ini/common.go index 0b8a9249..5a9d4813 100644 --- a/lib/ini/common.go +++ b/lib/ini/common.go @@ -43,3 +43,99 @@ func IsValueBoolTrue(v string) bool { } return false } + +// parseTag parse the ini field tag as used in the struct's field. +// This returned slice always have 4 string element: section, subsection, key, +// and default value. +func parseTag(in string) (tags []string) { + var ( + sb strings.Builder + r rune + x int + foundSep bool + isEsc bool + ) + + tags = append(tags, ``, ``, ``, ``) + + in = strings.TrimSpace(in) + if len(in) == 0 { + return tags + } + + // Parse the section. + for x, r = range in { + if r == ':' { + foundSep = true + break + } + } + if !foundSep { + // If no ":" found, the tag is the section. + tags[0] = in + return tags + } + + tags[0] = in[:x] + in = in[x+1:] + + // Parse the subsection. + foundSep = false + sb.Reset() + for x, r = range in { + if r == '\\' { + if isEsc { + sb.WriteRune('\\') + isEsc = false + } else { + isEsc = true + } + continue + } + if r == '"' { + if isEsc { + sb.WriteRune(r) + isEsc = false + continue + } + break + } + if r == ':' { + if isEsc { + sb.WriteRune(r) + isEsc = false + continue + } + foundSep = true + break + } + sb.WriteRune(r) + } + tags[1] = sb.String() + + if !foundSep { + return tags + } + in = in[x+1:] + + // Parse variable name. + foundSep = false + for x, r = range in { + if r == ':' { + foundSep = true + break + } + } + if !foundSep { + tags[2] = in + return tags + } + + tags[2] = in[:x] + in = in[x+1:] + + // The rest is the default value. + tags[3] = in + + return tags +} diff --git a/lib/ini/common_test.go b/lib/ini/common_test.go index 09dd33ee..470b02b1 100644 --- a/lib/ini/common_test.go +++ b/lib/ini/common_test.go @@ -5,8 +5,10 @@ package ini import ( + "reflect" "testing" + libreflect "github.com/shuLhan/share/lib/reflect" "github.com/shuLhan/share/lib/test" ) @@ -51,3 +53,74 @@ func TestIsValueBoolTrue(t *testing.T) { test.Assert(t, "", c.exp, got) } } + +func TestParseTag(t *testing.T) { + type testCase struct { + in string + exp []string + } + + var cases = []testCase{{ + in: `sec`, + exp: []string{`sec`, ``, ``, ``}, + }, { + in: `sec:sub`, + exp: []string{`sec`, `sub`, ``, ``}, + }, { + in: `sec:sub:var`, + exp: []string{`sec`, `sub`, `var`, ``}, + }, { + in: `sec:sub:var:def`, + exp: []string{`sec`, `sub`, `var`, `def`}, + }, { + in: `sec:sub \"\:\\ name:var`, + exp: []string{`sec`, `sub ":\ name`, `var`, ``}, + }} + + var ( + c testCase + got []string + ) + for _, c = range cases { + got = parseTag(c.in) + test.Assert(t, c.in, c.exp, got) + } +} + +func TestParseTag_fromStruct(t *testing.T) { + type ADT struct { + F1 int `ini:"a"` + F2 int `ini:"a:b"` + F3 int `ini:"a:b:c"` + F4 int `ini:"a:b:c:d"` + F5 int `ini:"a:b \\\"\\: c:d"` + } + + var ( + exp = [][]string{ + {`a`, ``, ``, ``}, + {`a`, `b`, ``, ``}, + {`a`, `b`, `c`, ``}, + {`a`, `b`, `c`, `d`}, + {`a`, `b ": c`, `d`, ``}, + } + + adt ADT + vtype reflect.Type + field reflect.StructField + tag string + got []string + x int + ) + + vtype = reflect.TypeOf(adt) + + for x = 0; x < vtype.NumField(); x++ { + field = vtype.Field(x) + + tag, _, _ = libreflect.Tag(field, "ini") + + got = parseTag(tag) + test.Assert(t, tag, exp[x], got) + } +} diff --git a/lib/ini/doc.go b/lib/ini/doc.go index d7cf62e3..d6aad18d 100644 --- a/lib/ini/doc.go +++ b/lib/ini/doc.go @@ -121,8 +121,19 @@ // # Marshaling // // The container to be passed when marshaling must be struct type. -// Each exported field in the struct with "ini" tags will be marshaled based -// on the section, subsection, and key in the tag. +// Each exported field in the struct with "ini" tag is marshaled based +// on the section, subsection, and key in the tag's value. +// +// The "ini" tag syntax is, +// +// [SECTION] [':' SUBSECTION] [':' VAR] +// +// At least one of the section, subsection, or key should be defined. +// +// The subsection can contain colon and double quote. +// A colon `:` is escaped using double backslash `\\`, for example `a:b\\:c:d` +// contains section `a`, subsection `b:c`, and variable `d`. +// A double quote `"` is escaped using triple backslash, for example `\\\"`. // // If the field type is slice of primitive, for example "[]int", it will be // marshaled into multiple key with the same name. diff --git a/lib/ini/ini.go b/lib/ini/ini.go index f3edb073..c32a59f4 100644 --- a/lib/ini/ini.go +++ b/lib/ini/ini.go @@ -140,23 +140,12 @@ func (in *Ini) marshalStruct( var sec, sub, key, value string - tags = strings.Split(tag, fieldTagSeparator) - - switch len(tags) { - case 0: - if kind != reflect.Struct { - continue - } - case 1: - sec = tags[0] - key = field.Name - case 2: - sec = tags[0] - sub = tags[1] + tags = parseTag(tag) + sec = tags[0] + sub = tags[1] + if len(tags[2]) == 0 { key = field.Name - default: - sec = tags[0] - sub = tags[1] + } else { key = tags[2] } if len(parentSec) > 0 { @@ -165,7 +154,6 @@ func (in *Ini) marshalStruct( if len(parentSub) > 0 { sub = parentSub } - key = strings.ToLower(key) for kind == reflect.Ptr { diff --git a/lib/ini/ini_test.go b/lib/ini/ini_test.go index e6788718..f404f136 100644 --- a/lib/ini/ini_test.go +++ b/lib/ini/ini_test.go @@ -7,7 +7,6 @@ package ini import ( "bytes" "fmt" - "strings" "testing" "time" @@ -559,12 +558,8 @@ func TestIni_Get(t *testing.T) { continue } - tags = strings.Split(string(key), fieldTagSeparator) - if len(tags) >= 4 { - def = tags[3] - } else { - def = "" - } + tags = parseTag(string(key)) + def = tags[3] got, ok = cfg.Get(tags[0], tags[1], tags[2], def) got = fmt.Sprintf("%t %s.", ok, got) diff --git a/lib/ini/tag_struct_field.go b/lib/ini/tag_struct_field.go index ab739b63..7d99c9e4 100644 --- a/lib/ini/tag_struct_field.go +++ b/lib/ini/tag_struct_field.go @@ -17,6 +17,10 @@ type tagStructField map[string]*structField // subsection, and/or key along with their reflect type and value into // structField. func unpackTagStructField(rtype reflect.Type, rval reflect.Value) (out tagStructField) { + var ( + tags []string + ) + numField := rtype.NumField() if numField == 0 { return nil @@ -70,20 +74,14 @@ func unpackTagStructField(rtype reflect.Type, rval reflect.Value) (out tagStruct sfield.layout = time.RFC3339 } - tags := strings.Split(tag, fieldTagSeparator) + tags = parseTag(tag) + sfield.sec = tags[0] + sfield.sub = tags[1] + sfield.key = strings.ToLower(tags[2]) - switch len(tags) { - case 1: - sfield.sec = tags[0] - sfield.key = sfield.fname - case 2: + if len(sfield.key) == 0 { sfield.sec = tags[0] - sfield.sub = tags[1] sfield.key = sfield.fname - default: - sfield.sec = tags[0] - sfield.sub = tags[1] - sfield.key = strings.ToLower(tags[2]) } out[tag] = sfield |
