From 6596f277ee26474eb083003c7589fff46406c3a9 Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Fri, 8 Nov 2024 01:25:15 +0300 Subject: [PATCH 01/12] chore: add miniparse pkg for future .mini config files --- pkg/miniparse/miniparse.go | 132 ++++++++++++++++++++++++++++++++ pkg/miniparse/miniparse_test.go | 29 +++++++ 2 files changed, 161 insertions(+) create mode 100644 pkg/miniparse/miniparse.go create mode 100644 pkg/miniparse/miniparse_test.go diff --git a/pkg/miniparse/miniparse.go b/pkg/miniparse/miniparse.go new file mode 100644 index 0000000..e46dcd8 --- /dev/null +++ b/pkg/miniparse/miniparse.go @@ -0,0 +1,132 @@ +package miniparse + +import ( + "bufio" + "errors" + "fmt" + "io" + "unicode" +) + +const bufSize = 512 + +type mode int + +const ( + modeInit mode = iota + modeComment + modeSection + modeSectionEnd + modeKey + modeEqual + modeSpaceR + modeValue +) + +var ( + ErrLeadingSpace = errors.New("leading spaces are not allowed") + ErrInvalidChar = errors.New("invalid leading char") + ErrExpectedNewLine = errors.New("expected new line") + ErrInvalidSection = errors.New("invalid section name") + ErrInvalidKey = errors.New("invalid key name") + ErrExpectedEqual = errors.New("expected equal sign") + ErrExpectedSpace = errors.New("expected space") +) + +// Decode .mini config file into value using reflect. +// The mini format is similar to ini, but very strict. +func Decode(reader io.Reader, _ any) error { //nolint:funlen,gocognit,cyclop // TODO: refactor + r := bufio.NewReader(reader) + var m mode + var sec, key, val string + buf := make([]rune, 0, bufSize) + + for { + c, _, err := r.ReadRune() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return err + } + switch m { + case modeInit: + switch { + case c == '#': + m = modeComment + case c == '[': + m = modeSection + case isValidVar(c, true): + m = modeKey + buf = append(buf, c) + case c == '\n': + case unicode.IsSpace(c): + return ErrLeadingSpace + default: + return fmt.Errorf("%w: %c", ErrInvalidChar, c) + } + case modeComment: + if c == '\n' { + m = modeInit + } + case modeSection: + switch { + case isValidVar(c, false): + buf = append(buf, c) + case c == ']': + m = modeSectionEnd + default: + return fmt.Errorf("%w, found: %c", ErrInvalidSection, c) + } + case modeSectionEnd: + if c != '\n' { + return fmt.Errorf("%w, found: %c", ErrExpectedNewLine, c) + } + m = modeInit + sec = string(buf) + buf = buf[:0] + println("section:", sec) //nolint:forbidigo // ! remove + case modeKey: + switch { + case isValidVar(c, false): + buf = append(buf, c) + case c == ' ': + m = modeEqual + key = string(buf) + println("key:", key) //nolint:forbidigo // ! remove + buf = buf[:0] + default: + return fmt.Errorf("%w, found: %c", ErrInvalidKey, c) + } + case modeEqual: + if c != '=' { + return fmt.Errorf("%w, found: %c", ErrExpectedEqual, c) + } + m = modeSpaceR + case modeSpaceR: + if c != ' ' { + return fmt.Errorf("%w, found: %c", ErrExpectedSpace, c) + } + m = modeValue + case modeValue: + if c == '\n' { + m = modeInit + val = string(buf) + println("val:", val) //nolint:forbidigo // ! remove + buf = buf[:0] + } else { + buf = append(buf, c) + } + } + } + + return nil +} + +func isValidVar(c rune, first bool) bool { + if c >= unicode.MaxASCII { + return false + } + + return c == '_' || unicode.IsLower(c) || (!first && unicode.IsDigit(c)) +} diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go new file mode 100644 index 0000000..f0b91f8 --- /dev/null +++ b/pkg/miniparse/miniparse_test.go @@ -0,0 +1,29 @@ +package miniparse_test + +import ( + "strings" + "testing" + + "github.com/Gornak40/algolymp/pkg/miniparse" + "github.com/stretchr/testify/require" +) + +const coreMini = `[core] +id = avx2024 +name = AVX инструкции с нуля 🦍 +number_id = 2812 +number_id = 1233 +` + +func TestSimple(t *testing.T) { + t.Parallel() + r := strings.NewReader(coreMini) + var ss struct { + Core struct { + ID string `mini:"id"` + Name string `mini:"name"` + } `mini:"core"` + } + + require.NoError(t, miniparse.Decode(r, &ss)) +} From dc465d8f6df467a43379198435c986d27e35e512 Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Fri, 8 Nov 2024 12:54:02 +0300 Subject: [PATCH 02/12] chore: refactor to func state machine --- pkg/miniparse/miniparse.go | 103 ++----------------------- pkg/miniparse/miniparse_test.go | 2 + pkg/miniparse/statemachine.go | 128 ++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 97 deletions(-) create mode 100644 pkg/miniparse/statemachine.go diff --git a/pkg/miniparse/miniparse.go b/pkg/miniparse/miniparse.go index e46dcd8..7446cd6 100644 --- a/pkg/miniparse/miniparse.go +++ b/pkg/miniparse/miniparse.go @@ -3,24 +3,7 @@ package miniparse import ( "bufio" "errors" - "fmt" "io" - "unicode" -) - -const bufSize = 512 - -type mode int - -const ( - modeInit mode = iota - modeComment - modeSection - modeSectionEnd - modeKey - modeEqual - modeSpaceR - modeValue ) var ( @@ -35,11 +18,10 @@ var ( // Decode .mini config file into value using reflect. // The mini format is similar to ini, but very strict. -func Decode(reader io.Reader, _ any) error { //nolint:funlen,gocognit,cyclop // TODO: refactor +func Decode(reader io.Reader, _ any) error { r := bufio.NewReader(reader) - var m mode - var sec, key, val string - buf := make([]rune, 0, bufSize) + m := newMachine() + nxt := m.stateInit for { c, _, err := r.ReadRune() @@ -49,84 +31,11 @@ func Decode(reader io.Reader, _ any) error { //nolint:funlen,gocognit,cyclop // if err != nil { return err } - switch m { - case modeInit: - switch { - case c == '#': - m = modeComment - case c == '[': - m = modeSection - case isValidVar(c, true): - m = modeKey - buf = append(buf, c) - case c == '\n': - case unicode.IsSpace(c): - return ErrLeadingSpace - default: - return fmt.Errorf("%w: %c", ErrInvalidChar, c) - } - case modeComment: - if c == '\n' { - m = modeInit - } - case modeSection: - switch { - case isValidVar(c, false): - buf = append(buf, c) - case c == ']': - m = modeSectionEnd - default: - return fmt.Errorf("%w, found: %c", ErrInvalidSection, c) - } - case modeSectionEnd: - if c != '\n' { - return fmt.Errorf("%w, found: %c", ErrExpectedNewLine, c) - } - m = modeInit - sec = string(buf) - buf = buf[:0] - println("section:", sec) //nolint:forbidigo // ! remove - case modeKey: - switch { - case isValidVar(c, false): - buf = append(buf, c) - case c == ' ': - m = modeEqual - key = string(buf) - println("key:", key) //nolint:forbidigo // ! remove - buf = buf[:0] - default: - return fmt.Errorf("%w, found: %c", ErrInvalidKey, c) - } - case modeEqual: - if c != '=' { - return fmt.Errorf("%w, found: %c", ErrExpectedEqual, c) - } - m = modeSpaceR - case modeSpaceR: - if c != ' ' { - return fmt.Errorf("%w, found: %c", ErrExpectedSpace, c) - } - m = modeValue - case modeValue: - if c == '\n' { - m = modeInit - val = string(buf) - println("val:", val) //nolint:forbidigo // ! remove - buf = buf[:0] - } else { - buf = append(buf, c) - } + nxt, err = nxt(c) + if err != nil { + return err } } return nil } - -func isValidVar(c rune, first bool) bool { - if c >= unicode.MaxASCII { - return false - } - - return c == '_' || unicode.IsLower(c) || (!first && unicode.IsDigit(c)) -} diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go index f0b91f8..1783601 100644 --- a/pkg/miniparse/miniparse_test.go +++ b/pkg/miniparse/miniparse_test.go @@ -9,8 +9,10 @@ import ( ) const coreMini = `[core] +# year is just for fun id = avx2024 name = AVX инструкции с нуля 🦍 + number_id = 2812 number_id = 1233 ` diff --git a/pkg/miniparse/statemachine.go b/pkg/miniparse/statemachine.go new file mode 100644 index 0000000..692ba03 --- /dev/null +++ b/pkg/miniparse/statemachine.go @@ -0,0 +1,128 @@ +package miniparse + +import ( + "fmt" + "unicode" +) + +const bufSize = 512 + +type machine struct { + buf []rune + sec string + key string + val string +} + +func newMachine() *machine { + return &machine{ + buf: make([]rune, 0, bufSize), + } +} + +type stateFunc func(c rune) (stateFunc, error) + +func (m *machine) stateInit(c rune) (stateFunc, error) { + switch { + case c == '#': + return m.stateComment, nil + case c == '[': + return m.stateSection, nil + case isValidVar(c, true): + m.buf = append(m.buf, c) + + return m.stateKey, nil + case c == '\n': + return m.stateInit, nil + case unicode.IsSpace(c): + return nil, ErrLeadingSpace + default: + return nil, fmt.Errorf("%w: %c", ErrInvalidChar, c) + } +} + +func (m *machine) stateComment(c rune) (stateFunc, error) { + if c == '\n' { + return m.stateInit, nil + } + + return m.stateComment, nil +} + +func (m *machine) stateSection(c rune) (stateFunc, error) { + switch { + case isValidVar(c, false): + m.buf = append(m.buf, c) + + return m.stateSection, nil + case c == ']': + return m.stateSectionEnd, nil + default: + return nil, fmt.Errorf("%w, found: %c", ErrInvalidSection, c) + } +} + +func (m *machine) stateSectionEnd(c rune) (stateFunc, error) { + if c != '\n' { + return nil, fmt.Errorf("%w, found: %c", ErrExpectedNewLine, c) + } + m.sec = string(m.buf) + println("sec:", m.sec) //nolint:forbidigo // ! remove + m.buf = m.buf[:0] + + return m.stateInit, nil +} + +func (m *machine) stateKey(c rune) (stateFunc, error) { + switch { + case isValidVar(c, false): + m.buf = append(m.buf, c) + + return m.stateKey, nil + case c == ' ': + m.key = string(m.buf) + println("key:", m.key) //nolint:forbidigo // ! remove + m.buf = m.buf[:0] + + return m.stateEqualSign, nil + default: + return nil, fmt.Errorf("%w, found: %c", ErrInvalidKey, c) + } +} + +func (m *machine) stateEqualSign(c rune) (stateFunc, error) { + if c != '=' { + return nil, fmt.Errorf("%w, found: %c", ErrExpectedEqual, c) + } + + return m.stateSpaceR, nil +} + +func (m *machine) stateSpaceR(c rune) (stateFunc, error) { + if c != ' ' { + return nil, fmt.Errorf("%w, found: %c", ErrExpectedSpace, c) + } + + return m.stateValue, nil +} + +func (m *machine) stateValue(c rune) (stateFunc, error) { + if c == '\n' { + m.val = string(m.buf) + println("val:", m.val) //nolint:forbidigo // ! remove + m.buf = m.buf[:0] + + return m.stateInit, nil + } + m.buf = append(m.buf, c) + + return m.stateValue, nil +} + +func isValidVar(c rune, first bool) bool { + if c >= unicode.MaxASCII { + return false + } + + return c == '_' || unicode.IsLower(c) || (!first && unicode.IsDigit(c)) +} From 679a1ac506fd40a05c88ec5854cd0fae4c504692 Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Fri, 8 Nov 2024 15:17:15 +0300 Subject: [PATCH 03/12] chore: support reflection for one struct --- pkg/miniparse/miniparse.go | 19 ++++- pkg/miniparse/miniparse_test.go | 42 ++++++++-- pkg/miniparse/reflection.go | 138 ++++++++++++++++++++++++++++++++ pkg/miniparse/statemachine.go | 23 +++--- 4 files changed, 202 insertions(+), 20 deletions(-) create mode 100644 pkg/miniparse/reflection.go diff --git a/pkg/miniparse/miniparse.go b/pkg/miniparse/miniparse.go index 7446cd6..12b3be7 100644 --- a/pkg/miniparse/miniparse.go +++ b/pkg/miniparse/miniparse.go @@ -4,6 +4,7 @@ import ( "bufio" "errors" "io" + "reflect" ) var ( @@ -14,17 +15,23 @@ var ( ErrInvalidKey = errors.New("invalid key name") ErrExpectedEqual = errors.New("expected equal sign") ErrExpectedSpace = errors.New("expected space") + ErrUnexpectedEOF = errors.New("unexpected end of file") + + ErrExpectedPointer = errors.New("expected not nil pointer") + ErrExpectedStruct = errors.New("expected struct") + ErrBadRecordType = errors.New("bad record type") + ErrExpectedArray = errors.New("expected array") ) // Decode .mini config file into value using reflect. // The mini format is similar to ini, but very strict. -func Decode(reader io.Reader, _ any) error { - r := bufio.NewReader(reader) +func Decode(r io.Reader, v any) error { + rb := bufio.NewReader(r) m := newMachine() nxt := m.stateInit for { - c, _, err := r.ReadRune() + c, _, err := rb.ReadRune() if errors.Is(err, io.EOF) { break } @@ -36,6 +43,10 @@ func Decode(reader io.Reader, _ any) error { return err } } + // TODO: find more Go-like solution + if reflect.ValueOf(nxt).Pointer() != reflect.ValueOf(m.stateInit).Pointer() { + return ErrUnexpectedEOF + } - return nil + return m.feed(v) } diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go index 1783601..db3cf15 100644 --- a/pkg/miniparse/miniparse_test.go +++ b/pkg/miniparse/miniparse_test.go @@ -15,17 +15,47 @@ name = AVX инструкции с нуля 🦍 number_id = 2812 number_id = 1233 +public = 1 +contest_id = 33 +magic = -1 + +flags = false +flags = true +flags = T + +contest_id = 12312 +contest_id = 9012 ` func TestSimple(t *testing.T) { t.Parallel() - r := strings.NewReader(coreMini) - var ss struct { - Core struct { - ID string `mini:"id"` - Name string `mini:"name"` - } `mini:"core"` + type core struct { + ID string `mini:"id"` + Name string `mini:"name"` + NumberID []int `mini:"number_id"` + Public bool `mini:"public"` + Empty string + Flags []bool `mini:"flags"` + ContestID []string `mini:"contest_id"` + Magic int `mini:"magic"` } + type config struct { + Core core `mini:"core"` + } + var ss config + r := strings.NewReader(coreMini) require.NoError(t, miniparse.Decode(r, &ss)) + require.Equal(t, config{ + Core: core{ + ID: "avx2024", + Name: "AVX инструкции с нуля 🦍", + NumberID: []int{2812, 1233}, + Public: true, + Empty: "", + Flags: []bool{false, true, true}, + ContestID: []string{"33", "12312", "9012"}, + Magic: -1, + }, + }, ss) } diff --git a/pkg/miniparse/reflection.go b/pkg/miniparse/reflection.go new file mode 100644 index 0000000..677c9c0 --- /dev/null +++ b/pkg/miniparse/reflection.go @@ -0,0 +1,138 @@ +package miniparse + +import ( + "fmt" + "reflect" + "strconv" +) + +const ( + tagName = "mini" +) + +func (m *machine) feed(v any) error { + pv := reflect.ValueOf(v) + if pv.Kind() != reflect.Pointer || pv.IsNil() { + return ErrExpectedPointer + } + vv := pv.Elem() + vt := vv.Type() + if vv.Kind() != reflect.Struct { + return ErrExpectedStruct + } + for i := range vv.NumField() { + tf := vt.Field(i) + vf := vv.Field(i) + if err := m.parseField(tf, vf); err != nil { + return err + } + } + + return nil +} + +func (m *machine) parseField(f reflect.StructField, v reflect.Value) error { + name := f.Tag.Get(tagName) + if name == "" { + return nil + } + r, ok := m.data[name] + if !ok { + return nil + } + if v.Kind() != reflect.Struct { // TODO: support arrays + return fmt.Errorf("%w: field %s", ErrExpectedStruct, name) + } + + return writeRecord(r[0], v) // TODO: fix it +} + +func writeRecord(r record, v reflect.Value) error { + t := v.Type() + for i := range v.NumField() { + tf := t.Field(i) + name := tf.Tag.Get(tagName) + if name == "" { + continue + } + a, ok := r[name] + if !ok { + continue + } + if tf.Type.Kind() != reflect.Slice && len(a) > 1 { + return fmt.Errorf("%w: field %s", ErrExpectedArray, name) + } + val, err := parseValue(a, tf.Type) + if err != nil { + return err + } + v.Field(i).Set(val) + } + + return nil +} + +func parseValue(a []string, t reflect.Type) (reflect.Value, error) { + switch t { + case reflect.TypeOf(""): + return reflect.ValueOf(a[0]), nil + case reflect.TypeOf([]string{}): + return reflect.ValueOf(a), nil + case reflect.TypeOf(0): + x, err := toInts(a) + if err != nil { + return reflect.Value{}, err + } + + return reflect.ValueOf(x[0]), nil + case reflect.TypeOf([]int{}): + x, err := toInts(a) + if err != nil { + return reflect.Value{}, err + } + + return reflect.ValueOf(x), nil + case reflect.TypeOf(false): + x, err := toBools(a) + if err != nil { + return reflect.Value{}, err + } + + return reflect.ValueOf(x[0]), nil + case reflect.TypeOf([]bool{}): + x, err := toBools(a) + if err != nil { + return reflect.Value{}, err + } + + return reflect.ValueOf(x), nil + default: + return reflect.Value{}, fmt.Errorf("%w: %s", ErrBadRecordType, t.String()) + } +} + +func toInts(a []string) ([]int, error) { + ra := make([]int, 0, len(a)) + for _, s := range a { + x, err := strconv.Atoi(s) + if err != nil { + return nil, err + } + ra = append(ra, x) + } + + return ra, nil +} + +func toBools(a []string) ([]bool, error) { + ra := make([]bool, 0, len(a)) + for _, s := range a { + x, err := strconv.ParseBool(s) + if err != nil { + return nil, err + } + ra = append(ra, x) + } + + return ra, nil +} diff --git a/pkg/miniparse/statemachine.go b/pkg/miniparse/statemachine.go index 692ba03..e5aab25 100644 --- a/pkg/miniparse/statemachine.go +++ b/pkg/miniparse/statemachine.go @@ -7,16 +7,19 @@ import ( const bufSize = 512 +type record map[string][]string + type machine struct { - buf []rune - sec string - key string - val string + buf []rune + data map[string][]record + cur record + key string } func newMachine() *machine { return &machine{ - buf: make([]rune, 0, bufSize), + buf: make([]rune, 0, bufSize), + data: make(map[string][]record), } } @@ -66,8 +69,9 @@ func (m *machine) stateSectionEnd(c rune) (stateFunc, error) { if c != '\n' { return nil, fmt.Errorf("%w, found: %c", ErrExpectedNewLine, c) } - m.sec = string(m.buf) - println("sec:", m.sec) //nolint:forbidigo // ! remove + sec := string(m.buf) + m.cur = make(record) + m.data[sec] = append(m.data[sec], m.cur) m.buf = m.buf[:0] return m.stateInit, nil @@ -81,7 +85,6 @@ func (m *machine) stateKey(c rune) (stateFunc, error) { return m.stateKey, nil case c == ' ': m.key = string(m.buf) - println("key:", m.key) //nolint:forbidigo // ! remove m.buf = m.buf[:0] return m.stateEqualSign, nil @@ -108,8 +111,8 @@ func (m *machine) stateSpaceR(c rune) (stateFunc, error) { func (m *machine) stateValue(c rune) (stateFunc, error) { if c == '\n' { - m.val = string(m.buf) - println("val:", m.val) //nolint:forbidigo // ! remove + val := string(m.buf) + m.cur[m.key] = append(m.cur[m.key], val) m.buf = m.buf[:0] return m.stateInit, nil From 9df5cd1d4534f7dd076f1cdb944ee928baf99049 Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Fri, 8 Nov 2024 17:36:45 +0300 Subject: [PATCH 04/12] test: add parse errors test --- pkg/miniparse/miniparse_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go index db3cf15..86e542a 100644 --- a/pkg/miniparse/miniparse_test.go +++ b/pkg/miniparse/miniparse_test.go @@ -59,3 +59,35 @@ func TestSimple(t *testing.T) { }, }, ss) } + +// TODO: require section first. +// TODO: section name first letter. +func TestParseErrors(t *testing.T) { + t.Parallel() + var ss struct{} + tt := map[string]error{ + "[html page]": miniparse.ErrInvalidSection, + "[htmlPage]": miniparse.ErrInvalidSection, + "[html]": miniparse.ErrUnexpectedEOF, + "[html": miniparse.ErrUnexpectedEOF, + "[html]\n]": miniparse.ErrInvalidChar, + "[html]\nkey\n": miniparse.ErrInvalidKey, + "[html]\nkey=value\n": miniparse.ErrInvalidKey, + "[html]\nKEY = value\n": miniparse.ErrInvalidChar, + "[html]\n1key = value\n": miniparse.ErrInvalidChar, + "[html]\nkey= value\n": miniparse.ErrInvalidKey, + "[html]\nkey =value\n": miniparse.ErrExpectedSpace, + "[html]\nkey = value": miniparse.ErrUnexpectedEOF, + "[html]\nkey = value[html]\n[html]": miniparse.ErrUnexpectedEOF, + "[html] # section\n": miniparse.ErrExpectedNewLine, + "# section\n[html]\nkey = 1\nkey2\n": miniparse.ErrInvalidKey, + "[html]\nkey = \"bababababayka\nbrrry\"\n": miniparse.ErrInvalidKey, + " [html]\n": miniparse.ErrLeadingSpace, + "[html]\n key = value\n": miniparse.ErrLeadingSpace, + } + + for s, err := range tt { + r := strings.NewReader(s) + require.ErrorIs(t, miniparse.Decode(r, &ss), err) + } +} From e1f0b945badc929065a91601b54802806dd36c9f Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Fri, 8 Nov 2024 18:15:40 +0300 Subject: [PATCH 05/12] test: more corner cases; fix: panic without root section --- pkg/miniparse/miniparse.go | 1 + pkg/miniparse/miniparse_test.go | 27 +++++++++++++++++++++++---- pkg/miniparse/statemachine.go | 18 +++++++++++++++++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/pkg/miniparse/miniparse.go b/pkg/miniparse/miniparse.go index 12b3be7..ef03d7b 100644 --- a/pkg/miniparse/miniparse.go +++ b/pkg/miniparse/miniparse.go @@ -12,6 +12,7 @@ var ( ErrInvalidChar = errors.New("invalid leading char") ErrExpectedNewLine = errors.New("expected new line") ErrInvalidSection = errors.New("invalid section name") + ErrRootSection = errors.New("expected root section") ErrInvalidKey = errors.New("invalid key name") ErrExpectedEqual = errors.New("expected equal sign") ErrExpectedSpace = errors.New("expected space") diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go index 86e542a..b6185d7 100644 --- a/pkg/miniparse/miniparse_test.go +++ b/pkg/miniparse/miniparse_test.go @@ -60,14 +60,30 @@ func TestSimple(t *testing.T) { }, ss) } -// TODO: require section first. -// TODO: section name first letter. +func TestCorner(t *testing.T) { + t.Parallel() + var ss struct{} + tt := []string{ + "", + "[alone]\n", + "\n", + "[empty]\n[empty]\n[again]\n", + "[rune]\nkey = [section]\n[section]\nkey2 = 0\n", + } + + for _, s := range tt { + r := strings.NewReader(s) + require.NoError(t, miniparse.Decode(r, &ss)) + } +} + func TestParseErrors(t *testing.T) { t.Parallel() var ss struct{} tt := map[string]error{ - "[html page]": miniparse.ErrInvalidSection, - "[htmlPage]": miniparse.ErrInvalidSection, + "[html page]\n": miniparse.ErrInvalidSection, + "[htmlPage]\n": miniparse.ErrInvalidSection, + "[1html]\n": miniparse.ErrInvalidSection, "[html]": miniparse.ErrUnexpectedEOF, "[html": miniparse.ErrUnexpectedEOF, "[html]\n]": miniparse.ErrInvalidChar, @@ -78,12 +94,15 @@ func TestParseErrors(t *testing.T) { "[html]\nkey= value\n": miniparse.ErrInvalidKey, "[html]\nkey =value\n": miniparse.ErrExpectedSpace, "[html]\nkey = value": miniparse.ErrUnexpectedEOF, + "[html]\nkey = value\n": miniparse.ErrExpectedEqual, "[html]\nkey = value[html]\n[html]": miniparse.ErrUnexpectedEOF, "[html] # section\n": miniparse.ErrExpectedNewLine, "# section\n[html]\nkey = 1\nkey2\n": miniparse.ErrInvalidKey, "[html]\nkey = \"bababababayka\nbrrry\"\n": miniparse.ErrInvalidKey, " [html]\n": miniparse.ErrLeadingSpace, "[html]\n key = value\n": miniparse.ErrLeadingSpace, + "[html]\n = value\n": miniparse.ErrLeadingSpace, + "key = value\n": miniparse.ErrRootSection, } for s, err := range tt { diff --git a/pkg/miniparse/statemachine.go b/pkg/miniparse/statemachine.go index e5aab25..80e2254 100644 --- a/pkg/miniparse/statemachine.go +++ b/pkg/miniparse/statemachine.go @@ -30,8 +30,11 @@ func (m *machine) stateInit(c rune) (stateFunc, error) { case c == '#': return m.stateComment, nil case c == '[': - return m.stateSection, nil + return m.stateSection1, nil case isValidVar(c, true): + if m.cur == nil { + return nil, fmt.Errorf("%w, got %c", ErrRootSection, c) + } m.buf = append(m.buf, c) return m.stateKey, nil @@ -52,6 +55,19 @@ func (m *machine) stateComment(c rune) (stateFunc, error) { return m.stateComment, nil } +func (m *machine) stateSection1(c rune) (stateFunc, error) { + switch { + case isValidVar(c, true): + m.buf = append(m.buf, c) + + return m.stateSection, nil + case c == ']': + return nil, fmt.Errorf("%w: empty", ErrInvalidSection) + default: + return nil, fmt.Errorf("%w, found: %c", ErrInvalidSection, c) + } +} + func (m *machine) stateSection(c rune) (stateFunc, error) { switch { case isValidVar(c, false): From 23df3dd4854a986ea1fe80285c9561e9b5cd4912 Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Fri, 8 Nov 2024 18:22:04 +0300 Subject: [PATCH 06/12] chore: minor reflect patch --- pkg/miniparse/miniparse_test.go | 2 +- pkg/miniparse/reflection.go | 20 ++++++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go index b6185d7..55951d6 100644 --- a/pkg/miniparse/miniparse_test.go +++ b/pkg/miniparse/miniparse_test.go @@ -27,7 +27,7 @@ contest_id = 12312 contest_id = 9012 ` -func TestSimple(t *testing.T) { +func TestReflect(t *testing.T) { t.Parallel() type core struct { ID string `mini:"id"` diff --git a/pkg/miniparse/reflection.go b/pkg/miniparse/reflection.go index 677c9c0..e190c6f 100644 --- a/pkg/miniparse/reflection.go +++ b/pkg/miniparse/reflection.go @@ -79,31 +79,27 @@ func parseValue(a []string, t reflect.Type) (reflect.Value, error) { case reflect.TypeOf([]string{}): return reflect.ValueOf(a), nil case reflect.TypeOf(0): - x, err := toInts(a) - if err != nil { - return reflect.Value{}, err - } - - return reflect.ValueOf(x[0]), nil + fallthrough case reflect.TypeOf([]int{}): x, err := toInts(a) if err != nil { return reflect.Value{}, err } + if t.Kind() != reflect.Slice { + return reflect.ValueOf(x[0]), nil + } return reflect.ValueOf(x), nil case reflect.TypeOf(false): - x, err := toBools(a) - if err != nil { - return reflect.Value{}, err - } - - return reflect.ValueOf(x[0]), nil + fallthrough case reflect.TypeOf([]bool{}): x, err := toBools(a) if err != nil { return reflect.Value{}, err } + if t.Kind() != reflect.Slice { + return reflect.ValueOf(x[0]), nil + } return reflect.ValueOf(x), nil default: From 60f8046ae1da3f4b59b436a67d9ca5b18adc4212 Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Fri, 8 Nov 2024 19:19:03 +0300 Subject: [PATCH 07/12] test: add reflection tests --- pkg/miniparse/miniparse_test.go | 54 +++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go index 55951d6..e3e2522 100644 --- a/pkg/miniparse/miniparse_test.go +++ b/pkg/miniparse/miniparse_test.go @@ -77,7 +77,7 @@ func TestCorner(t *testing.T) { } } -func TestParseErrors(t *testing.T) { +func TestParseError(t *testing.T) { t.Parallel() var ss struct{} tt := map[string]error{ @@ -105,8 +105,56 @@ func TestParseErrors(t *testing.T) { "key = value\n": miniparse.ErrRootSection, } - for s, err := range tt { + for s, terr := range tt { r := strings.NewReader(s) - require.ErrorIs(t, miniparse.Decode(r, &ss), err) + require.ErrorIs(t, miniparse.Decode(r, &ss), terr) } } + +func TestReflectArgError(t *testing.T) { + t.Parallel() + + m := make(map[string]any) + require.ErrorIs(t, miniparse.Decode(strings.NewReader(""), m), miniparse.ErrExpectedPointer) + require.ErrorIs(t, miniparse.Decode(strings.NewReader(""), &m), miniparse.ErrExpectedStruct) + require.ErrorIs(t, miniparse.Decode(strings.NewReader(""), nil), miniparse.ErrExpectedPointer) + + x := 4 + require.ErrorIs(t, miniparse.Decode(strings.NewReader(""), &x), miniparse.ErrExpectedStruct) + require.ErrorIs(t, miniparse.Decode(strings.NewReader(""), "gorill"), miniparse.ErrExpectedPointer) + + var ss struct{} + require.ErrorIs(t, miniparse.Decode(strings.NewReader(""), ss), miniparse.ErrExpectedPointer) +} + +const htmlMini = `[html] +page_id = 14141 +preload = true +# wow! a comment +page_id = 99119 +name = Bob +` + +func TestReflectError(t *testing.T) { + t.Parallel() + r := strings.NewReader(htmlMini) + + var ss2 struct { + HTML struct { + PageID []struct { + Key string `mini:"key"` + Value string `mini:"value"` + } `mini:"page_id"` + } `mini:"html"` + } + require.ErrorIs(t, miniparse.Decode(r, &ss2), miniparse.ErrBadRecordType) + + var ss3 struct { + HTML struct { + Name string `mini:"name"` + PageID int `mini:"page_id"` + } `mini:"html"` + } + r.Reset(htmlMini) + require.ErrorIs(t, miniparse.Decode(r, &ss3), miniparse.ErrExpectedArray) +} From 22257fc37e8ba92ea8346b49eb349ac8358ca26c Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Fri, 8 Nov 2024 19:59:57 +0300 Subject: [PATCH 08/12] feat: support section arrays; test: more reflection --- pkg/miniparse/miniparse.go | 1 + pkg/miniparse/miniparse_test.go | 39 +++++++++++++++++++++++++++++---- pkg/miniparse/reflection.go | 23 ++++++++++++++++--- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/pkg/miniparse/miniparse.go b/pkg/miniparse/miniparse.go index ef03d7b..2ce0460 100644 --- a/pkg/miniparse/miniparse.go +++ b/pkg/miniparse/miniparse.go @@ -20,6 +20,7 @@ var ( ErrExpectedPointer = errors.New("expected not nil pointer") ErrExpectedStruct = errors.New("expected struct") + ErrBadSectionType = errors.New("bad section type") ErrBadRecordType = errors.New("bad record type") ErrExpectedArray = errors.New("expected array") ) diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go index e3e2522..a42be2e 100644 --- a/pkg/miniparse/miniparse_test.go +++ b/pkg/miniparse/miniparse_test.go @@ -25,6 +25,16 @@ flags = T contest_id = 12312 contest_id = 9012 + +[standings] +id = avx2024_fall +type = acm +# visible names +public = true + +[standings] +id = avx2024_aesc +type = acm ` func TestReflect(t *testing.T) { @@ -39,8 +49,14 @@ func TestReflect(t *testing.T) { ContestID []string `mini:"contest_id"` Magic int `mini:"magic"` } + type standings struct { + ID string `mini:"id"` + Type string `mini:"type"` + Public bool `mini:"public"` + } type config struct { - Core core `mini:"core"` + Core core `mini:"core"` + Standings []standings `mini:"standings"` } var ss config @@ -52,11 +68,21 @@ func TestReflect(t *testing.T) { Name: "AVX инструкции с нуля 🦍", NumberID: []int{2812, 1233}, Public: true, - Empty: "", Flags: []bool{false, true, true}, ContestID: []string{"33", "12312", "9012"}, Magic: -1, }, + Standings: []standings{ + { + ID: "avx2024_fall", + Type: "acm", + Public: true, + }, + { + ID: "avx2024_aesc", + Type: "acm", + }, + }, }, ss) } @@ -138,7 +164,6 @@ name = Bob func TestReflectError(t *testing.T) { t.Parallel() r := strings.NewReader(htmlMini) - var ss2 struct { HTML struct { PageID []struct { @@ -149,12 +174,18 @@ func TestReflectError(t *testing.T) { } require.ErrorIs(t, miniparse.Decode(r, &ss2), miniparse.ErrBadRecordType) + r.Reset(htmlMini) var ss3 struct { HTML struct { Name string `mini:"name"` PageID int `mini:"page_id"` } `mini:"html"` } - r.Reset(htmlMini) require.ErrorIs(t, miniparse.Decode(r, &ss3), miniparse.ErrExpectedArray) + + r.Reset(htmlMini) + var ss4 struct { + HTML []map[string]any `mini:"html"` + } + require.ErrorIs(t, miniparse.Decode(r, &ss4), miniparse.ErrExpectedStruct) } diff --git a/pkg/miniparse/reflection.go b/pkg/miniparse/reflection.go index e190c6f..ddb2874 100644 --- a/pkg/miniparse/reflection.go +++ b/pkg/miniparse/reflection.go @@ -40,11 +40,28 @@ func (m *machine) parseField(f reflect.StructField, v reflect.Value) error { if !ok { return nil } - if v.Kind() != reflect.Struct { // TODO: support arrays - return fmt.Errorf("%w: field %s", ErrExpectedStruct, name) + if f.Type.Kind() != reflect.Slice && len(r) > 1 { + return fmt.Errorf("%w: field %s", ErrExpectedArray, name) } + switch f.Type.Kind() { //nolint:exhaustive // all those cases go to default + case reflect.Struct: + return writeRecord(r[0], v) + case reflect.Slice: + if f.Type.Elem().Kind() != reflect.Struct { + return fmt.Errorf("%w: field %s", ErrExpectedStruct, name) + } + v.Set(reflect.MakeSlice(f.Type, len(r), len(r))) + for i, rv := range r { + elem := v.Index(i) + if err := writeRecord(rv, elem); err != nil { + return err + } + } - return writeRecord(r[0], v) // TODO: fix it + return nil + default: + return fmt.Errorf("%w: field %s", ErrBadSectionType, name) + } } func writeRecord(r record, v reflect.Value) error { From 1ea676e678c696bf5451cdb5a68ee36cf42aa93d Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Sat, 9 Nov 2024 01:29:54 +0300 Subject: [PATCH 09/12] feat: add mini-required tag --- .golangci.yml | 1 + pkg/miniparse/miniparse.go | 1 + pkg/miniparse/miniparse_test.go | 24 ++++++++++++++++++++++-- pkg/miniparse/reflection.go | 19 ++++++++++++++----- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index c220ad5..7d2eaed 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,6 +11,7 @@ linters: - varnamelen - godox - exportloopref + - tagalign # alignment looks awful linters-settings: cyclop: diff --git a/pkg/miniparse/miniparse.go b/pkg/miniparse/miniparse.go index 2ce0460..76321a4 100644 --- a/pkg/miniparse/miniparse.go +++ b/pkg/miniparse/miniparse.go @@ -23,6 +23,7 @@ var ( ErrBadSectionType = errors.New("bad section type") ErrBadRecordType = errors.New("bad record type") ErrExpectedArray = errors.New("expected array") + ErrRequiredField = errors.New("field marked required") ) // Decode .mini config file into value using reflect. diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go index a42be2e..2ad041a 100644 --- a/pkg/miniparse/miniparse_test.go +++ b/pkg/miniparse/miniparse_test.go @@ -40,7 +40,7 @@ type = acm func TestReflect(t *testing.T) { t.Parallel() type core struct { - ID string `mini:"id"` + ID string `mini:"id" mini-required:"true"` Name string `mini:"name"` NumberID []int `mini:"number_id"` Public bool `mini:"public"` @@ -50,7 +50,7 @@ func TestReflect(t *testing.T) { Magic int `mini:"magic"` } type standings struct { - ID string `mini:"id"` + ID string `mini:"id" mini-required:"true"` Type string `mini:"type"` Public bool `mini:"public"` } @@ -188,4 +188,24 @@ func TestReflectError(t *testing.T) { HTML []map[string]any `mini:"html"` } require.ErrorIs(t, miniparse.Decode(r, &ss4), miniparse.ErrExpectedStruct) + + r.Reset(htmlMini) + var ss5 struct { + HTML struct { + } `mini:"html"` + CSS []struct { + } `mini:"css" mini-required:"true"` + } + require.ErrorIs(t, miniparse.Decode(r, &ss5), miniparse.ErrRequiredField) + + r.Reset(htmlMini) + var ss6 struct { + HTML struct { + Name string `mini:"name" mini-required:"true"` + ID int `mini:"id" mini-required:"true"` + } `mini:"html"` + CSS []struct { + } `mini:"css"` + } + require.ErrorIs(t, miniparse.Decode(r, &ss6), miniparse.ErrRequiredField) } diff --git a/pkg/miniparse/reflection.go b/pkg/miniparse/reflection.go index ddb2874..66d0f0d 100644 --- a/pkg/miniparse/reflection.go +++ b/pkg/miniparse/reflection.go @@ -7,7 +7,8 @@ import ( ) const ( - tagName = "mini" + tagName = "mini" + tagRequired = "mini-required" ) func (m *machine) feed(v any) error { @@ -32,12 +33,16 @@ func (m *machine) feed(v any) error { } func (m *machine) parseField(f reflect.StructField, v reflect.Value) error { - name := f.Tag.Get(tagName) - if name == "" { + name, ok := f.Tag.Lookup(tagName) + if !ok { return nil } r, ok := m.data[name] if !ok { + if _, ok := f.Tag.Lookup(tagRequired); ok { + return fmt.Errorf("%w: field %s", ErrRequiredField, name) + } + return nil } if f.Type.Kind() != reflect.Slice && len(r) > 1 { @@ -68,12 +73,16 @@ func writeRecord(r record, v reflect.Value) error { t := v.Type() for i := range v.NumField() { tf := t.Field(i) - name := tf.Tag.Get(tagName) - if name == "" { + name, ok := tf.Tag.Lookup(tagName) + if !ok { continue } a, ok := r[name] if !ok { + if _, ok := tf.Tag.Lookup(tagRequired); ok { + return fmt.Errorf("%w: field %s", ErrRequiredField, name) + } + continue } if tf.Type.Kind() != reflect.Slice && len(a) > 1 { From 7de3b7c5ebc410058e6011c374e9ab8085fbf61e Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Sat, 9 Nov 2024 01:46:36 +0300 Subject: [PATCH 10/12] feat: add duration reflect type --- pkg/miniparse/miniparse_test.go | 28 +++++++++++++++++++++++----- pkg/miniparse/reflection.go | 27 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go index 2ad041a..4d956ad 100644 --- a/pkg/miniparse/miniparse_test.go +++ b/pkg/miniparse/miniparse_test.go @@ -3,6 +3,7 @@ package miniparse_test import ( "strings" "testing" + "time" "github.com/Gornak40/algolymp/pkg/miniparse" "github.com/stretchr/testify/require" @@ -35,6 +36,12 @@ public = true [standings] id = avx2024_aesc type = acm +refresh_delay = 60s + +[penalty] +id = avx2024_pen +ban = 1440m +ban = 72h ` func TestReflect(t *testing.T) { @@ -50,13 +57,19 @@ func TestReflect(t *testing.T) { Magic int `mini:"magic"` } type standings struct { - ID string `mini:"id" mini-required:"true"` - Type string `mini:"type"` - Public bool `mini:"public"` + ID string `mini:"id" mini-required:"true"` + Type string `mini:"type"` + Public bool `mini:"public"` + RefreshDelay time.Duration `mini:"refresh_delay"` + } + type penalty struct { + ID string `mini:"id" mini-required:"true"` + Ban []time.Duration `mini:"ban"` } type config struct { Core core `mini:"core"` Standings []standings `mini:"standings"` + Penalty penalty `mini:"penalty"` } var ss config @@ -79,10 +92,15 @@ func TestReflect(t *testing.T) { Public: true, }, { - ID: "avx2024_aesc", - Type: "acm", + ID: "avx2024_aesc", + Type: "acm", + RefreshDelay: time.Minute, }, }, + Penalty: penalty{ + ID: "avx2024_pen", + Ban: []time.Duration{24 * time.Hour, 72 * time.Hour}, + }, }, ss) } diff --git a/pkg/miniparse/reflection.go b/pkg/miniparse/reflection.go index 66d0f0d..ddbd827 100644 --- a/pkg/miniparse/reflection.go +++ b/pkg/miniparse/reflection.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" "strconv" + "time" ) const ( @@ -98,6 +99,7 @@ func writeRecord(r record, v reflect.Value) error { return nil } +//nolint:cyclop // it's ok, nothing to worry about func parseValue(a []string, t reflect.Type) (reflect.Value, error) { switch t { case reflect.TypeOf(""): @@ -127,6 +129,18 @@ func parseValue(a []string, t reflect.Type) (reflect.Value, error) { return reflect.ValueOf(x[0]), nil } + return reflect.ValueOf(x), nil + case reflect.TypeOf(time.Second): + fallthrough + case reflect.TypeOf([]time.Duration{}): + x, err := toDurations(a) + if err != nil { + return reflect.Value{}, err + } + if t.Kind() != reflect.Slice { + return reflect.ValueOf(x[0]), nil + } + return reflect.ValueOf(x), nil default: return reflect.Value{}, fmt.Errorf("%w: %s", ErrBadRecordType, t.String()) @@ -158,3 +172,16 @@ func toBools(a []string) ([]bool, error) { return ra, nil } + +func toDurations(a []string) ([]time.Duration, error) { + ra := make([]time.Duration, 0, len(a)) + for _, s := range a { + x, err := time.ParseDuration(s) + if err != nil { + return nil, err + } + ra = append(ra, x) + } + + return ra, nil +} From 9143784c06d950e1777115a8e86ff7e870437451 Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Sat, 9 Nov 2024 12:07:53 +0300 Subject: [PATCH 11/12] feat: add mini-default tag --- pkg/miniparse/miniparse_test.go | 73 ++++++++++++++++++++------------- pkg/miniparse/reflection.go | 9 ++-- 2 files changed, 51 insertions(+), 31 deletions(-) diff --git a/pkg/miniparse/miniparse_test.go b/pkg/miniparse/miniparse_test.go index 4d956ad..0f90df2 100644 --- a/pkg/miniparse/miniparse_test.go +++ b/pkg/miniparse/miniparse_test.go @@ -42,35 +42,44 @@ refresh_delay = 60s id = avx2024_pen ban = 1440m ban = 72h + +[penalty] +id = avx2024_aesc_pen +value = 100 ` +type core struct { + ID string `mini:"id" mini-required:"true"` + Name string `mini:"name"` + NumberID []int `mini:"number_id"` + Public bool `mini:"public"` + Empty string + Flags []bool `mini:"flags"` + ContestID []string `mini:"contest_id"` + Magic int `mini:"magic"` +} + +type standings struct { + ID string `mini:"id" mini-required:"true"` + Type string `mini:"type"` + Public bool `mini:"public"` + RefreshDelay time.Duration `mini:"refresh_delay"` +} + +type penalty struct { + ID string `mini:"id" mini-required:"true"` + Ban []time.Duration `mini:"ban" mini-default:"3h30m"` + Value int `mini:"value" mini-default:"50"` +} + +type config struct { + Core core `mini:"core"` + Standings []standings `mini:"standings"` + Penalty []penalty `mini:"penalty"` +} + func TestReflect(t *testing.T) { t.Parallel() - type core struct { - ID string `mini:"id" mini-required:"true"` - Name string `mini:"name"` - NumberID []int `mini:"number_id"` - Public bool `mini:"public"` - Empty string - Flags []bool `mini:"flags"` - ContestID []string `mini:"contest_id"` - Magic int `mini:"magic"` - } - type standings struct { - ID string `mini:"id" mini-required:"true"` - Type string `mini:"type"` - Public bool `mini:"public"` - RefreshDelay time.Duration `mini:"refresh_delay"` - } - type penalty struct { - ID string `mini:"id" mini-required:"true"` - Ban []time.Duration `mini:"ban"` - } - type config struct { - Core core `mini:"core"` - Standings []standings `mini:"standings"` - Penalty penalty `mini:"penalty"` - } var ss config r := strings.NewReader(coreMini) @@ -97,9 +106,17 @@ func TestReflect(t *testing.T) { RefreshDelay: time.Minute, }, }, - Penalty: penalty{ - ID: "avx2024_pen", - Ban: []time.Duration{24 * time.Hour, 72 * time.Hour}, + Penalty: []penalty{ + { + ID: "avx2024_pen", + Ban: []time.Duration{24 * time.Hour, 72 * time.Hour}, + Value: 50, + }, + { + ID: "avx2024_aesc_pen", + Ban: []time.Duration{210 * time.Minute}, + Value: 100, + }, }, }, ss) } diff --git a/pkg/miniparse/reflection.go b/pkg/miniparse/reflection.go index ddbd827..418c34c 100644 --- a/pkg/miniparse/reflection.go +++ b/pkg/miniparse/reflection.go @@ -10,6 +10,7 @@ import ( const ( tagName = "mini" tagRequired = "mini-required" + tagDefault = "mini-default" ) func (m *machine) feed(v any) error { @@ -80,11 +81,13 @@ func writeRecord(r record, v reflect.Value) error { } a, ok := r[name] if !ok { - if _, ok := tf.Tag.Lookup(tagRequired); ok { + if def, ok := tf.Tag.Lookup(tagDefault); ok { + a = []string{def} + } else if _, ok := tf.Tag.Lookup(tagRequired); ok { return fmt.Errorf("%w: field %s", ErrRequiredField, name) + } else { + continue } - - continue } if tf.Type.Kind() != reflect.Slice && len(a) > 1 { return fmt.Errorf("%w: field %s", ErrExpectedArray, name) From aa4808e3eb7cde1025fd7c7effd2cc72513b8752 Mon Sep 17 00:00:00 2001 From: Gornak40 Date: Sat, 9 Nov 2024 12:29:55 +0300 Subject: [PATCH 12/12] docs: add docs to Decode func --- pkg/miniparse/miniparse.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/miniparse/miniparse.go b/pkg/miniparse/miniparse.go index 76321a4..815d47e 100644 --- a/pkg/miniparse/miniparse.go +++ b/pkg/miniparse/miniparse.go @@ -28,6 +28,23 @@ var ( // Decode .mini config file into value using reflect. // The mini format is similar to ini, but very strict. +// +// Each line of the config must be one of the following: blank, comment, record title, record field. +// Leading spaces are not allowed. End-of-line spaces are only allowed in record field lines. +// A non-empty .mini config file must end with a blank string. +// A comment must begin with a '#' character. All comments will be ignored by the parser. +// The record title must be "[title]", where title is the non-empty name of the varname. +// Varnames contain only lowercase ascii letters, digits and the '_' character. +// The first letter of the varname must not be a digit. +// The record field must have the form “key = value”, where key is a non-empty varname. +// The value contains any valid utf-8 sequence. +// Record names and keys can be non-unique. Then they will be interpreted as arrays. +// +// The mini format does not specify data types of values. +// But this decoder works only with string, int, bool and time.Duration. +// You should use `mini:"name"` tag to designate a structure field. +// You can use the `mini-required:"true"` tag for mandatory fields. +// You can use the `mini-default:"value"` tag for default values. func Decode(r io.Reader, v any) error { rb := bufio.NewReader(r) m := newMachine()