Skip to content

Commit

Permalink
up: update some parse logic, add more unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
inhere committed Aug 18, 2022
1 parent 45c679b commit 716bdfb
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 123 deletions.
8 changes: 4 additions & 4 deletions lexer.go → _example/lexer.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package properties
package _example

import (
"fmt"
Expand Down Expand Up @@ -71,6 +71,6 @@ func (l *lexer) parse() error {
return nil
}

func (l *lexer) next() tokenItem {
return tokenItem{}
}
// func (l *lexer) next() tokenItem {
// return tokenItem{}
// }
2 changes: 2 additions & 0 deletions encode.go → encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ func (e *Encoder) encode(mp map[string]interface{}) ([]byte, error) {
var path string
var buf bytes.Buffer

// TODO sort keys

// TODO...
for name, val := range mp {
path = name
Expand Down
20 changes: 20 additions & 0 deletions encoder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package properties_test

import (
"testing"

"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/testutil/assert"
"github.com/gookit/properties"
)

func TestEncode(t *testing.T) {
e := properties.NewEncoder()
bs, err := e.Marshal(maputil.Data{
"name": "inhere",
"age": 234,
})

assert.NoErr(t, err)
assert.NotEmpty(t, bs)
}
2 changes: 1 addition & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func ExampleMarshal() {

fmt.Println(string(bts))

// Output:
// Output like:
// name=inhere
// age=300
}
Expand Down
89 changes: 89 additions & 0 deletions options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package properties_test

import (
"testing"
"time"

"github.com/gookit/goutil/strutil"
"github.com/gookit/goutil/testutil/assert"
"github.com/gookit/properties"
)

func TestOptions_InlineComment(t *testing.T) {
text := `
key = value # inline comments
key2 = value2 // inline comments
key3 = value3
`

p := properties.NewParser()
err := p.Parse(text)
assert.NoErr(t, err)
assert.Eq(t, "value # inline comments", p.Str("key"))

p = properties.NewParser(func(opts *properties.Options) {
opts.InlineComment = true
})
err = p.Parse(text)
assert.NoErr(t, err)
assert.Eq(t, "value", p.Str("key"))
}

func TestParseTime(t *testing.T) {
text := `
// properties string
name = inhere
age = 200
expire = 3s
`

type MyConf struct {
Name string `properties:"name"`
Age int `properties:"age"`
Expire time.Duration `properties:"expire"`
}

p, err := properties.Parse(text, properties.ParseTime)
assert.NoErr(t, err)

cfg := &MyConf{}
err = p.MapStruct("", cfg)
assert.NoErr(t, err)
assert.Eq(t, "inhere", cfg.Name)
assert.Eq(t, 3*time.Second, cfg.Expire)

err = p.MapStruct("not-found", cfg)
assert.Err(t, err)
}

func TestOptions_BeforeCollect(t *testing.T) {
text := `
// properties string
name = inhere
age = 200
expire = 3s
`

type MyConf struct {
Name string `properties:"name"`
Age int `properties:"age"`
Expire time.Duration `properties:"expire"`
}

// tests Unmarshal, BeforeCollect
p := properties.NewParser(properties.ParseTime, func(opts *properties.Options) {
opts.BeforeCollect = func(name string, val interface{}) interface{} {
if name == "name" {
return strutil.Upper(val.(string))
}
return val
}
})

cfg := &MyConf{}
err := p.Unmarshal([]byte(text), cfg)
assert.NoErr(t, err)
assert.Eq(t, "INHERE", cfg.Name)
assert.Eq(t, 3*time.Second, cfg.Expire)

}
120 changes: 16 additions & 104 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,93 +24,12 @@ const (
VarRefStartChars = "${"
)

const (
// TokInvalid invalid token
TokInvalid rune = 0
// TokOLComments one line comments start by !,#
TokOLComments = 'c'
// TokMLComments multi line comments by /* */
TokMLComments = 'C'
// TokILComments inline comments
TokILComments = 'i'
// TokValueLine value line
TokValueLine = 'v'
// TokMLValMarkS multi line value by single quotes: '''
TokMLValMarkS = 'm'
// TokMLValMarkD multi line value by double quotes: """
TokMLValMarkD = 'M'
// TokMLValMarkQ multi line value by left slash quote: \
TokMLValMarkQ = 'q'
)

// TokString name
func TokString(tok rune) string {
switch tok {
case TokOLComments:
return "LINE_COMMENT"
case TokILComments:
return "INLINE_COMMENT"
case TokMLComments:
return "MLINE_COMMENT"
case TokValueLine:
return "VALUE_LINE"
case TokMLValMarkS, TokMLValMarkD, TokMLValMarkQ:
return "MLINE_VALUE"
default:
return "INVALID"
}
}

type tokenItem struct {
// see TokValueLine
kind rune
// key path string. eg: top.sub.some-key
path string
keys []string

// token value
value string
// for multi line value.
values []string
// for multi line comments.
comments []string
}

func newTokenItem(path, value string, kind rune) *tokenItem {
tk := &tokenItem{
kind: kind,
value: value,
}

tk.setPath(path)
return tk
}

func (ti *tokenItem) setPath(path string) {
// TODO check path valid
ti.path = path

if strings.ContainsRune(path, '.') {
ti.keys = strings.Split(path, ".")
}
}

// Valid of the token data.
func (ti *tokenItem) addValue(val string) {
ti.values = append(ti.values, val)
}

// Valid of the token data.
func (ti *tokenItem) Valid() bool {
return ti.kind != 0
}

// Parser for parse properties contents
type Parser struct {
maputil.Data
// last parse error
err error
lex *lexer
// lex *lexer
// text string
opts *Options
// key path map
Expand Down Expand Up @@ -145,20 +64,22 @@ func (p *Parser) Unmarshal(v []byte, ptr interface{}) error {
if err := p.ParseBytes(v); err != nil {
return err
}

return p.MapStruct("", ptr)
}

// Parse text contents
func (p *Parser) Parse(text string) error {
if text = strings.TrimSpace(text); text == "" {
return errors.New("cannot input empty string to parse")
return errors.New("cannot input empty contents to parse")
}
return p.ParseFrom(strings.NewReader(text))
}

// ParseBytes text contents
func (p *Parser) ParseBytes(bs []byte) error {
if len(bs) == 0 {
return errors.New("cannot input empty contents to parse")
}
return p.ParseFrom(bytes.NewReader(bs))
}

Expand Down Expand Up @@ -204,8 +125,8 @@ func (p *Parser) ParseFrom(r io.Reader) error {
if strings.HasSuffix(str, MultiLineValMarkS) { // end
tok = TokInvalid
val += str[:ln-3]
// p.smap[key] = val
p.setValue(key, val, "")
p.setValue(key, val, comments)
comments = "" // reset
} else {
val += str + "\n"
}
Expand All @@ -217,8 +138,8 @@ func (p *Parser) ParseFrom(r io.Reader) error {
if strings.HasSuffix(str, MultiLineValMarkD) { // end
tok = TokInvalid
val += str[:ln-3]
// p.smap[key] = val
p.setValue(key, val, "")
p.setValue(key, val, comments)
comments = "" // reset
} else {
val += str + "\n"
}
Expand All @@ -232,8 +153,8 @@ func (p *Parser) ParseFrom(r io.Reader) error {
} else {
tok = TokInvalid
val += str
// p.smap[key] = val
p.setValue(key, val, "")
p.setValue(key, val, comments)
comments = "" // reset
}
continue
}
Expand All @@ -247,7 +168,7 @@ func (p *Parser) ParseFrom(r io.Reader) error {

if str[0] == '/' {
if ln < 2 {
p.err = errorx.Rawf("invalid string %q, at line#%d", str, line)
p.err = errorx.Rawf("invalid contents %q, at line#%d", str, line)
continue
}

Expand Down Expand Up @@ -284,13 +205,13 @@ func (p *Parser) ParseFrom(r io.Reader) error {

nodes := strutil.SplitNTrimmed(str, "=", 2)
if len(nodes) != 2 {
p.err = errorx.Rawf("invalid format(key=val): %q, at line#%d", str, line)
p.err = errorx.Rawf("invalid contents %q(should be KEY=VALUE), at line#%d", str, line)
continue
}

key, val = nodes[0], nodes[1]
if len(key) == 0 {
p.err = errorx.Rawf("key is empty: %q, at line#%d", str, line)
p.err = errorx.Rawf("key cannot be empty: %q, at line#%d", str, line)
continue
}

Expand Down Expand Up @@ -335,12 +256,8 @@ func (p *Parser) ParseFrom(r io.Reader) error {
}
}

if len(comments) > 0 {
p.comments[key] = comments
comments = "" // reset
}

p.setValue(key, val, "")
p.setValue(key, val, comments)
comments = "" // reset
}

return p.err
Expand Down Expand Up @@ -453,11 +370,6 @@ func (p *Parser) MapStruct(key string, ptr interface{}) error {
return err
}

// Err last parse error
func (p *Parser) Err() error {
return p.err
}

// SMap data
func (p *Parser) SMap() maputil.SMap {
return p.smap
Expand Down
22 changes: 21 additions & 1 deletion parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ comments
*/
top2.sub.key2-other = has-char
# comments 2
// comments 2
top.sub.key3 = false
# slice list
Expand All @@ -61,6 +61,9 @@ value
properties.WithDebug,
properties.ParseEnv,
properties.ParseInlineSlice,
func(opts *properties.Options) {
opts.TrimValue = true
},
)
err := p.Parse(text)
assert.NoErr(t, err)
Expand Down Expand Up @@ -232,5 +235,22 @@ key1 = val2
assert.NotEmpty(t, smp)
assert.ContainsKeys(t, smp, []string{"key0", "key1", "top.sub2.mline1"})
assert.Eq(t, "multi line value", smp.Str("top.sub2.mline1"))
}

func TestParser_Parse_err(t *testing.T) {
p := properties.NewParser()
assert.ErrMsg(t, p.Parse(""), `cannot input empty contents to parse`)
assert.ErrMsg(t, p.ParseBytes(nil), `cannot input empty contents to parse`)

err := p.Parse("no-value")
assert.ErrMsg(t, err, `invalid contents "no-value"(should be KEY=VALUE), at line#1`)

err = p.Parse("/")
assert.ErrMsg(t, err, `invalid contents "/", at line#1`)

err = p.Parse("=value")
assert.ErrMsg(t, err, `key cannot be empty: "=value", at line#1`)

err = p.Unmarshal(nil, nil)
assert.ErrMsg(t, err, `cannot input empty contents to parse`)
}
Loading

0 comments on commit 716bdfb

Please sign in to comment.