diff --git a/README.md b/README.md index b26de99..e6b5279 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Config +# Config(v2) Nginx configuration style parser with golang # Install diff --git a/block.go b/block.go new file mode 100644 index 0000000..cde1131 --- /dev/null +++ b/block.go @@ -0,0 +1,96 @@ +package config + +import ( + "bytes" + "reflect" +) + +func (cfg *Config) createBlock(s *bytes.Buffer) error { + // fixed { be close to key like server{ + if cfg.searchKey && s.Len() > 0 { + if err := cfg.getElement(s.String()); err != nil { + return err + } + + s.Reset() + cfg.inSearchVal() + } + + // vars + vars := make(map[string]string) + cfg.vars = append(cfg.vars, cfg.currentVar) + cfg.currentVar = vars + + cfg.inBlock++ + // slice or map? + cfg.bkQueue = append(cfg.bkQueue, cfg.bkMulti) + cfg.bkMulti = false + if cfg.searchVal && s.Len() > 0 && cfg.current.Kind() == reflect.Map { + cfg.bkMulti = true + if cfg.current.IsNil() { + cfg.current.Set(reflect.MakeMap(cfg.current.Type())) + } + + v := reflect.New(cfg.current.Type().Key()) + cfg.pushElement(v) + err := cfg.set(s.String()) + if err != nil { + return err + } + cfg.mapKey = cfg.current + cfg.popElement() + val := reflect.New(cfg.current.Type().Elem()) + cfg.pushElement(val) + } + + if cfg.current.Kind() == reflect.Slice { + cfg.pushMultiBlock() + n := cfg.current.Len() + if cfg.current.Type().Elem().Kind() == reflect.Ptr { + ref := reflect.New(cfg.current.Type().Elem().Elem()) + if err := cfg.init(ref); err != nil { + return err + } + cfg.current.Set(reflect.Append(cfg.current, ref)) + } else { + ref := reflect.New(cfg.current.Type().Elem()) + if err := cfg.init(ref); err != nil { + return err + } + cfg.current.Set(reflect.Append(cfg.current, ref.Elem())) + } + cfg.pushElement(cfg.current.Index(n)) + } + cfg.inSearchKey() + s.Reset() + return nil +} + +func (cfg *Config) closeBlock(s *bytes.Buffer) { + if cfg.bkMulti { + val := cfg.current + cfg.popElement() + if cfg.current.Kind() == reflect.Map { + cfg.current.SetMapIndex(cfg.mapKey, val) + } + } + cfg.popMultiBlock() + + // vars + cfg.currentVar = cfg.vars[len(cfg.vars)-1] + cfg.vars = cfg.vars[:len(cfg.vars)-1] + + cfg.inBlock-- + cfg.popElement() + cfg.inSearchKey() +} + +func (cfg *Config) pushMultiBlock() { + cfg.bkQueue = append(cfg.bkQueue, cfg.bkMulti) + cfg.bkMulti = true +} + +func (cfg *Config) popMultiBlock() { + cfg.bkMulti = cfg.bkQueue[len(cfg.bkQueue)-1] + cfg.bkQueue = cfg.bkQueue[:len(cfg.bkQueue)-1] +} diff --git a/config.go b/config.go index 0ac0058..316f464 100644 --- a/config.go +++ b/config.go @@ -3,25 +3,27 @@ package config import ( "bufio" "bytes" - "errors" - "fmt" + "io" "os" "path/filepath" "reflect" - "strconv" "strings" - "unicode" - "time" + "sync" ) -type IConfigLog interface { +type Configurable struct { + Runnable bool + config reflect.Value } // Config A Config struct type Config struct { + sync.Mutex filename string queue []reflect.Value current reflect.Value + entry reflect.Value + typ reflect.StructField searchVal bool searchKey bool inBlock int @@ -37,136 +39,235 @@ type Config struct { vars []map[string]string currentVar map[string]string camel bool + directives map[string]*Configurable + cwd string + file *os.File + line int64 + stash []*stash + format *format + hook *hook } -// Cofnig.New create a config parser with filename +// New a config parser with filename func New(filename string) *Config { - return &Config{filename: filename, camel: true} + conf := &Config{filename: filename, camel: true} + conf.directives = make(map[string]*Configurable) + conf.format = &format{} + conf.hook = &hook{conf} + return conf } // Config.AutoCamel auto replace _ to camel -func (c *Config) AutoCamel(b bool) { - c.camel = b +func (cfg *Config) AutoCamel(b bool) { + cfg.camel = b +} + +// Config.Entry set an entry for parser +func (cfg *Config) Entry(entry interface{}) error { + if cfg.entry.IsValid() { + return cfg.error("entry already set") + } + var err error + cfg.entry, err = cfg.valueOf(entry) + if err != nil { + return err + } + + return nil +} + +// Config.Directive add a directive for paser +func (cfg *Config) Directive(directive string, conf interface{}) (*Configurable, error) { + cfg.Lock() + defer cfg.Unlock() + + directive = cfg.fixedField(directive) + rev, err := cfg.valueOf(conf) + if err != nil { + return nil, err + } + + config := &Configurable{Runnable: false, config: rev} + if _, ok := cfg.directives[directive]; ok { + return nil, cfg.error("directive [ %s ] duplication", directive) + } else { + cfg.directives[directive] = config + } + + return config, nil +} + +// Config.Parse parse config with Entry and directive from file +func (cfg *Config) Parse() error { + if !cfg.entry.IsValid() || cfg.entry.IsZero() { + return cfg.error("entry be required") + } + + cfg.current = reflect.Value{} + + cfg.reset() + + if err := cfg.parse(); err != nil { + return err + } + + cfg.current = reflect.Value{} + return nil } // Config.Unmarshal unmarshal config file to v -func (c *Config) Unmarshal(v interface{}) error { - rev := reflect.ValueOf(v) - if rev.Kind() != reflect.Ptr { - err := errors.New("non-pointer passed to Unmarshal") +func (cfg *Config) Unmarshal(v interface{}) error { + var err error + if cfg.current, err = cfg.valueOf(v); err != nil { return err } - c.current = rev.Elem() - c.inSearchKey() - c.inBlock = 0 - c.inInclude = 0 - c.currentVar = make(map[string]string) - c.searchVar = false - c.setVar = false - c.searchVarBlock = false - return c.parse() + + cfg.reset() + + return cfg.parse() } // Config.Reload reload config file -func (c *Config) Reload() error { - return c.parse() +func (cfg *Config) Reload() error { + cfg.reset() + return cfg.parse() } -func (c *Config) parse() error { +func (cfg *Config) parse() error { var err error var s bytes.Buffer var vs bytes.Buffer var vsb bytes.Buffer - if _, err = os.Stat(c.filename); os.IsNotExist(err) { - return err + if _, err = os.Stat(cfg.filename); os.IsNotExist(err) { + return cfg.error(err.Error()) } - var fp *os.File - fp, err = os.Open(c.filename) - defer fp.Close() + filename, err := filepath.Abs(cfg.filename) if err != nil { - return err + return cfg.error(err.Error()) + } else { + cfg.cwd = filepath.Dir(filename) } - reader := bufio.NewReader(fp) + var file *os.File + file, err = os.Open(filename) + if err != nil { + return cfg.error(err.Error()) + } + defer func() { + file.Close() + }() + + cfg.filename = file.Name() + cfg.file = file + + cfg.line = 1 + + reader := bufio.NewReader(cfg.file) var b byte - for err == nil { + for { b, err = reader.ReadByte() - if err == bufio.ErrBufferFull { - return nil + if err == io.EOF { + if !cfg.searchKey { + return cfg.error("invalid config vaild") + } + + if s.Len() > 0 { + return cfg.error("\"%s\" directive is not allowed here", s.String()) + } + + if err := cfg.popStash(); err != nil { + return err + } + break + } else if err != nil { + return cfg.error(err.Error()) } - if c.canSkip && b == '#' { + // bug in \r + if b == '\n' { + cfg.line++ + } + + if cfg.canSkip && b == '#' { reader.ReadLine() + cfg.line++ continue } - if c.canSkip && b == '/' { - if c.skip { + if cfg.canSkip && b == '/' { + if cfg.skip { reader.ReadLine() - c.skip = false + cfg.line++ + cfg.skip = false continue } - c.skip = true + cfg.skip = true continue } - if c.searchKey { - if c.delimiter(b) { + if cfg.searchKey { + if b == ';' { + return cfg.error("unknown directive \"" + s.String() + "\"") + } + + if cfg.delimiter(b) { if s.Len() > 0 { - c.inSearchVal() + cfg.inSearchVal() if strings.Compare(s.String(), "include") == 0 { s.Reset() - c.inInclude++ - if c.inInclude > 100 { - return errors.New("too many include, exceeds 100 limit!") + cfg.inInclude++ + if cfg.inInclude > 100 { + return cfg.error("too many include, exceeds 100 limit") } continue } else if strings.Compare(s.String(), "set") == 0 { s.Reset() - c.setVar = true + cfg.setVar = true continue } - c.getElement(s.String()) + if err := cfg.getElement(s.String()); err != nil { + return err + } s.Reset() } continue } } - if b == '{' && !c.searchVar && vs.Len() == 0 { - if err := c.createBlock(&s); err != nil { + if b == '{' && !cfg.searchVar && vs.Len() == 0 { + if err := cfg.createBlock(&s); err != nil { return err } continue } - if c.searchKey && b == '}' && c.inBlock > 0 { - c.closeBlock(&s) + if cfg.searchKey && b == '}' && cfg.inBlock > 0 { + cfg.closeBlock(&s) continue } - if c.searchVal { + if cfg.searchVal { if b == '$' { - c.searchVar = true + cfg.searchVar = true vs.Reset() vsb.Reset() vsb.WriteByte(b) continue } - if c.searchVar { + if cfg.searchVar { if b == '{' { if vs.Len() == 0 { - c.searchVarBlock = true + cfg.searchVarBlock = true vsb.WriteByte(b) continue } - if !c.searchVarBlock { - if !c.replace(&s, &vs) { + if !cfg.searchVarBlock { + if !cfg.replace(&s, &vs) { s.Write(vsb.Bytes()) } // is block? - if err := c.createBlock(&s); err != nil { + if err := cfg.createBlock(&s); err != nil { return err } else { continue @@ -174,18 +275,18 @@ func (c *Config) parse() error { } } // if b == '{' // Is space - if c.delimiter(b) { - if !c.replace(&s, &vs) { + if cfg.delimiter(b) { + if !cfg.replace(&s, &vs) { s.Write(vsb.Bytes()) } } } - if c.searchVarBlock && b == '}' { - c.searchVarBlock = false + if cfg.searchVarBlock && b == '}' { + cfg.searchVarBlock = false // replace $??? - c.searchVar = false - if !c.replace(&s, &vs) { + cfg.searchVar = false + if !cfg.replace(&s, &vs) { vsb.WriteByte(b) s.Write(vsb.Bytes()) } @@ -193,61 +294,70 @@ func (c *Config) parse() error { } if b == ';' { - // copy to c.current - c.inSearchKey() - if c.searchVar { - if c.searchVarBlock { - return errors.New(vsb.String() + " is not terminated by }") + if s.Len() < 1 { + return cfg.error("unknown value of directive \"" + s.String() + "\"") + } + // copy to cfg.current + cfg.inSearchKey() + if cfg.searchVar { + if cfg.searchVarBlock { + return cfg.error(vsb.String() + " is not terminated by }") } - c.searchVar = false - c.searchVarBlock = false - if !c.replace(&s, &vs) { + cfg.searchVar = false + cfg.searchVarBlock = false + if !cfg.replace(&s, &vs) { s.Write(vsb.Bytes()) } } // set to map - if c.setVar { + if cfg.setVar { sf := strings.Fields(s.String()) if len(sf) != 2 { - return errors.New("Invalid Config") + return cfg.error("set map with %s invalid", s.String()) } - c.currentVar[sf[0]] = sf[1] - c.setVar = false + cfg.currentVar[sf[0]] = sf[1] + cfg.setVar = false s.Reset() continue } - if c.inInclude > 0 { - c.filename = strings.TrimSpace(s.String()) + if cfg.inInclude > 0 { + cfg.filename = strings.TrimSpace(s.String()) s.Reset() - c.inInclude-- - files, err := filepath.Glob(c.filename) + cfg.inInclude-- + var files []string + if filepath.IsAbs(cfg.filename) { + files, err = filepath.Glob(cfg.filename) + } else { + files, err = filepath.Glob(filepath.Join(cfg.cwd, cfg.filename)) + } if err != nil { - return err + return cfg.error(err.Error()) } for _, file := range files { - c.filename = file - if err := c.parse(); err != nil { + cfg.pushStash() + cfg.filename = file + if err := cfg.parse(); err != nil { return err } } continue } - err := c.set(s.String()) + err := cfg.set(s.String()) if err != nil { return err } s.Reset() - c.popElement() + cfg.popElement() continue - } else if c.searchVar { // if b == ';' + } else if cfg.searchVar { // if b == ';' vs.WriteByte(b) vsb.WriteByte(b) - if !c.searchVarBlock { - c.replace(&s, &vs) + if !cfg.searchVarBlock { + cfg.replace(&s, &vs) } continue } @@ -256,348 +366,9 @@ func (c *Config) parse() error { s.WriteByte(b) } - if !c.searchKey && c.inBlock > 0 { - return errors.New("Invalid config file!") - } - - return nil -} - -func (c *Config) set(s string) error { - s = strings.TrimSpace(s) - if c.current.Kind() == reflect.Ptr { - if c.current.IsNil() { - c.current.Set(reflect.New(c.current.Type().Elem())) - } - c.current = c.current.Elem() - } - - switch c.current.Kind() { - case reflect.String: - c.current.SetString(c.clearQuoted(s)) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if c.current.Type().String() == "time.Duration" { - if time, err := time.ParseDuration(s);err != nil { - return err - }else{ - c.current.Set(reflect.ValueOf(time)) - } - }else{ - itmp, err := strconv.ParseInt(s, 10, c.current.Type().Bits()) - if err != nil { - return err - } - c.current.SetInt(itmp) - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - itmp, err := strconv.ParseUint(s, 10, c.current.Type().Bits()) - if err != nil { - return err - } - c.current.SetUint(itmp) - case reflect.Float32, reflect.Float64: - ftmp, err := strconv.ParseFloat(s, c.current.Type().Bits()) - if err != nil { - return err - } - c.current.SetFloat(ftmp) - case reflect.Bool: - if s == "yes" || s == "on" { - c.current.SetBool(true) - } else if s == "no" || s == "off" { - c.current.SetBool(false) - } else { - btmp, err := strconv.ParseBool(s) - if err != nil { - return err - } - c.current.SetBool(btmp) - } - case reflect.Slice: - sf, err := c.splitQuoted(s) - if err != nil { - return err - } - for _, sv := range sf { - n := c.current.Len() - c.current.Set(reflect.Append(c.current, reflect.Zero(c.current.Type().Elem()))) - c.pushElement(c.current.Index(n)) - c.set(sv) - c.popElement() - } - case reflect.Map: - if c.current.IsNil() { - c.current.Set(reflect.MakeMap(c.current.Type())) - } - - sf, err := c.splitQuoted(s) - if err != nil { - return err - } - if len(sf) != 2 { - return errors.New("Invalid Config") - } - var v reflect.Value - v = reflect.New(c.current.Type().Key()) - c.pushElement(v) - c.set(sf[0]) - key := c.current - c.popElement() - v = reflect.New(c.current.Type().Elem()) - c.pushElement(v) - c.set(sf[1]) - val := c.current - c.popElement() - - c.current.SetMapIndex(key, val) - default: - return errors.New(fmt.Sprintf("Invalid Type:%s", c.current.Kind())) - } - return nil -} - -func (c *Config) replace(s *bytes.Buffer, vs *bytes.Buffer) bool { - if vs.Len() == 0 { - return false - } - - for k, v := range c.currentVar { - if strings.Compare(k, vs.String()) == 0 { - // found - c.searchVar = false - s.WriteString(v) - vs.Reset() - return true - } + if !cfg.searchKey && cfg.inBlock > 0 { + return cfg.error("invalid config file") } - for i := len(c.vars) - 1; i >= 0; i-- { - for k, v := range c.vars[i] { - if strings.Compare(k, vs.String()) == 0 { - s.WriteString(v) - c.searchVar = false - vs.Reset() - return true - } - } - } - - return false -} - -func (c *Config) createBlock(s *bytes.Buffer) error { - // fixed { be close to key like server{ - if c.searchKey && s.Len() > 0 { - c.getElement(s.String()) - s.Reset() - c.inSearchVal() - } - - // vars - vars := make(map[string]string) - c.vars = append(c.vars, c.currentVar) - c.currentVar = vars - - c.inBlock++ - // slice or map? - c.bkQueue = append(c.bkQueue, c.bkMulti) - c.bkMulti = false - if c.searchVal && s.Len() > 0 && c.current.Kind() == reflect.Map { - c.bkMulti = true - if c.current.IsNil() { - c.current.Set(reflect.MakeMap(c.current.Type())) - } - var v reflect.Value - v = reflect.New(c.current.Type().Key()) - c.pushElement(v) - err := c.set(s.String()) - if err != nil { - return err - } - c.mapKey = c.current - c.popElement() - val := reflect.New(c.current.Type().Elem()) - c.pushElement(val) - } - - if c.current.Kind() == reflect.Slice { - c.pushMultiBlock() - n := c.current.Len() - if c.current.Type().Elem().Kind() == reflect.Ptr { - c.current.Set(reflect.Append(c.current, reflect.New(c.current.Type().Elem().Elem()))) - } else { - c.current.Set(reflect.Append(c.current, reflect.Zero(c.current.Type().Elem()))) - } - c.pushElement(c.current.Index(n)) - } - c.inSearchKey() - s.Reset() return nil } - -func (c *Config) closeBlock(s *bytes.Buffer) { - if c.bkMulti { - val := c.current - c.popElement() - if c.current.Kind() == reflect.Map { - c.current.SetMapIndex(c.mapKey, val) - } - } - c.popMultiBlock() - - // vars - c.currentVar = c.vars[len(c.vars)-1] - c.vars = c.vars[:len(c.vars)-1] - - c.inBlock-- - c.popElement() - c.inSearchKey() -} - -func (c *Config) inSearchKey() { - c.searchVal = false - c.searchKey = true - c.canSkip = true -} - -func (c *Config) inSearchVal() { - c.searchKey = false - c.searchVal = true - c.canSkip = false -} - -func (c *Config) getElement(s string) { - s = strings.TrimSpace(s) - if c.current.Kind() == reflect.Ptr { - if c.current.IsNil() { - c.current.Set(reflect.New(c.current.Type().Elem())) - } - c.current = c.current.Elem() - } - c.queue = append(c.queue, c.current) - // 如果是驼峰,就要处理一下 - if c.camel { - stmp := strings.Split(s, "_") - buffer := bytes.Buffer{} - for _, v := range stmp { - buffer.WriteString(strings.Title(v)) - } - c.current = c.current.FieldByName(buffer.String()) - }else{ - c.current = c.current.FieldByName(strings.Title(s)) - } -} - -func (c *Config) pushElement(v reflect.Value) { - c.queue = append(c.queue, c.current) - c.current = v -} - -func (c *Config) popElement() { - c.current = c.queue[len(c.queue)-1] - c.queue = c.queue[:len(c.queue)-1] -} - -func (c *Config) delimiter(b byte) bool { - return unicode.IsSpace(rune(b)) -} - -func (c *Config) pushMultiBlock() { - c.bkQueue = append(c.bkQueue, c.bkMulti) - c.bkMulti = true -} - -func (c *Config) popMultiBlock() { - c.bkMulti = c.bkQueue[len(c.bkQueue)-1] - c.bkQueue = c.bkQueue[:len(c.bkQueue)-1] -} - -func (c *Config) clearQuoted(s string) string { - s = strings.TrimSpace(s) - if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') { - return s[1 : len(s)-1] - } - - return s -} - -func (c *Config) splitQuoted(s string) ([]string, error) { - var sq []string - s = strings.TrimSpace(s) - var last_space bool = true - var need_space bool = true - var d_quote bool = false - var s_quote bool = false - var quote bool = false - var ch byte - var vs bytes.Buffer - - for i := 0; i < len(s); i++ { - ch = s[i] - - if quote { - quote = false - vs.WriteByte(ch) - continue - } - - if ch == '\\' { - quote = true - last_space = false - continue - } - - if last_space { - last_space = false - switch ch { - case '"': - d_quote = true - need_space = false - continue - case '\'': - s_quote = true - need_space = false - continue - case ' ': - last_space = true - continue - } - vs.WriteByte(ch) - } else { - if need_space && c.delimiter(ch) { - if vs.Len() > 0 { - sq = append(sq, vs.String()) - } - vs.Reset() - last_space = true - continue - } - - if d_quote { - if ch == '"' { - d_quote = false - need_space = true - continue - } - } else if s_quote { - if ch == '\'' { - s_quote = false - need_space = true - continue - } - } - - vs.WriteByte(ch) - } - } - - if quote || s_quote || d_quote { - return nil, errors.New(fmt.Sprintf("Invalid value: %v", s)) - } - - if vs.Len() > 0 { - sq = append(sq, vs.String()) - } - - return sq, nil -} diff --git a/element.go b/element.go new file mode 100644 index 0000000..50174f1 --- /dev/null +++ b/element.go @@ -0,0 +1,127 @@ +package config + +import ( + "bytes" + "reflect" + "strings" +) + +func (cfg *Config) getElement(s string) error { + cfg.Lock() + defer cfg.Unlock() + s = strings.TrimSpace(s) + + if !cfg.current.IsValid() { + if ok := cfg.fetchDirective(s); ok { + return nil + } else { + cfg.current = cfg.entry + } + } + + cfg.restoreElement() + + if cfg.current.Kind() != reflect.Struct { + return cfg.error("unknown directive %s, current kind is %s, but struct required", s, cfg.current.Kind()) + } + + // 如果是驼峰,就要处理一下 + field := cfg.fixedField(s) + + var ok bool + if cfg.typ, ok = cfg.current.Type().FieldByName(field); !ok { + cfg.popElement() + if cfg.current == cfg.entry { + if ok := cfg.fetchDirective(field); ok { + return nil + } + } + return cfg.error("unknown directive %s ", s) + } + + cfg.current = cfg.current.FieldByName(field) + + return nil +} + +func (cfg *Config) fetchDirective(s string) bool { + if rev, ok := cfg.directives[s]; ok { + rev.Runnable = true + cfg.current = rev.config + cfg.fixedElement() + return true + } + return false +} + +func (cfg *Config) getStruct() (reflect.Value, bool) { + if len(cfg.queue) < 1 { + return reflect.Value{}, false + } + return cfg.queue[len(cfg.queue)-1], true +} + +func (cfg *Config) getMethod(s string) (reflect.Value, bool) { + if element, ok := cfg.getStruct(); ok { + if element.Kind() != reflect.Ptr && element.CanAddr() { + element = element.Addr() + } + fn := element.MethodByName(s) + if fn.IsValid() && fn.Kind() == reflect.Func { + return fn, true + } + } + + return reflect.Value{}, false +} + +func (cfg *Config) getNearestSlice() (reflect.Value, bool) { + if len(cfg.queue) < 2 { + return reflect.Value{}, false + } + return cfg.queue[len(cfg.queue)-2], true +} + +func (cfg *Config) restoreElement() { + cfg.fixedElement() + cfg.queue = append(cfg.queue, cfg.current) +} + +func (cfg *Config) fixedElement() { + if cfg.current.Kind() == reflect.Ptr { + if cfg.current.IsNil() { + cfg.current.Set(reflect.New(cfg.current.Type().Elem())) + } + cfg.current = cfg.current.Elem() + } +} + +func (cfg *Config) pushElement(v reflect.Value) { + cfg.queue = append(cfg.queue, cfg.current) + cfg.current = v +} + +func (cfg *Config) popElement() { + if len(cfg.queue) < 1 { + cfg.current = reflect.Value{} + return + } + cfg.current = cfg.queue[len(cfg.queue)-1] + cfg.queue = cfg.queue[:len(cfg.queue)-1] +} + +func (cfg *Config) fixedField(s string) string { + if cfg.camel { + stmp := strings.Split(s, "_") + buffer := bytes.Buffer{} + for _, v := range stmp { + buffer.WriteString(strings.Title(v)) + } + + s = buffer.String() + } else { + s = strings.Title(s) + } + + return s +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..6e15aab --- /dev/null +++ b/error.go @@ -0,0 +1,10 @@ +package config + +import "fmt" + +func (cfg *Config) error(s string, a ...interface{}) error { + if len(a) > 0 { + s = fmt.Sprintf(s, a...) + } + return fmt.Errorf("%s in %s:%d", s, cfg.filename, cfg.line) +} diff --git a/format.go b/format.go new file mode 100644 index 0000000..eca74e3 --- /dev/null +++ b/format.go @@ -0,0 +1,135 @@ +package config + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" +) + +type format struct{} + +func (ft *format) Bytesize(s string) (int64, error) { + var multiplier int64 = 1 + u := s[len(s)-1] + + if strings.ToUpper(string(u)) == "B" { + s = s[:len(s)-1] + } + + u = s[len(s)-1] + switch strings.ToUpper(string(u)) { + case "K": + multiplier = 1024 + s = s[:len(s)-1] + case "M": + multiplier = 1048576 + s = s[:len(s)-1] + case "G": + multiplier = 1073741824 + s = s[:len(s)-1] + case "T": + multiplier = 1099511627776 + s = s[:len(s)-1] + } + + v, err := strconv.ParseInt(s, 10, 0) + if err != nil { + return 0, err + } + + v *= multiplier + return v, nil +} + +func (log *format) FileMode(s string) (os.FileMode, error) { + if len(s) != 4 { + return 0664, fmt.Errorf("invalid file mode") + } + + o := os.FileMode(s[3]) + o = o - 48 + + g := os.FileMode(s[2]) + g = g - 48 + + u := os.FileMode(s[1]) + u = u - 48 + + v := ((u & 7) << 6) | ((g & 7) << 3) | (o & 7) + + return os.FileMode(v), nil +} + +func (ft *format) Time(s string) (time.Duration, error) { + var unit string + var i int = len(s) - 1 + for ; i >= 0; i-- { + u := s[i] + if u > 47 && u < 58 { + i++ + break + } + } + + unit = string(s[i:]) + s = s[:i] + + v, err := strconv.ParseUint(s, 10, 0) + if err != nil { + return 0, err + } + t := time.Duration(v) + + switch unit { + case "ns": + t *= time.Nanosecond + case "us": + fallthrough + case "µs": + t *= time.Microsecond + case "ms": + t *= time.Millisecond + case "s": + fallthrough + case "S": + t *= time.Second + case "min": + t *= 60 * time.Second + case "H": + fallthrough + case "h": + t *= 3600 * time.Second + case "day": + fallthrough + case "D": + fallthrough + case "d": + t *= 86400 * time.Second + case "week": + fallthrough + case "w": + fallthrough + case "W": + t *= 604800 * time.Second + case "month": + fallthrough + case "m": + fallthrough + case "M": + t *= 2678400 * time.Second + case "year": + fallthrough + case "y": + fallthrough + case "Y": + t *= 31536000 * time.Second + case "": + t *= time.Second + default: + return 0, fmt.Errorf("invalid time unit") + } + + return t, nil +} diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..b4caedc --- /dev/null +++ b/helper.go @@ -0,0 +1,237 @@ +package config + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "unicode" +) + +func (cfg *Config) reset() { + cfg.inSearchKey() + cfg.inBlock = 0 + cfg.inInclude = 0 + cfg.currentVar = make(map[string]string) + cfg.searchVar = false + cfg.setVar = false + cfg.searchVarBlock = false + cfg.line = 1 +} + +func (cfg *Config) valueOf(conf interface{}) (reflect.Value, error) { + rev := reflect.ValueOf(conf) + if rev.Type().Name() == "Value" { + rev = rev.Interface().(reflect.Value) + } else if rev.Kind() != reflect.Ptr { + return reflect.Value{}, cfg.error("non-pointer and can't be addr") + } + + if err := cfg.init(rev); err != nil { + return reflect.Value{}, err + } + + rev = rev.Elem() + + return rev, nil +} + +func (cfg *Config) init(rev reflect.Value) error { + if rev.Kind() == reflect.Ptr { + rev = rev.Elem() + } + + if rev.Type().Kind() == reflect.Slice { + for i := 0; i < rev.Len(); i++ { + if err := cfg.init(rev.Index(i)); err != nil { + return err + } + } + return nil + } + + if rev.Type().Kind() != reflect.Struct { + return nil + } + + fn := rev.MethodByName("Init") + found := false + if fn.IsValid() && fn.Kind() == reflect.Func { + found = true + } + + if !found && rev.CanAddr() { + fn = rev.Addr().MethodByName("Init") + if fn.IsValid() && fn.Kind() == reflect.Func { + found = true + } + } + + if found { + if fn.Type().NumOut() != 1 { + return cfg.error("init in %s,invalid val kind, %s result required", rev.Type().String(), rev.Type().String()) + } + + if !fn.Type().Out(0).Implements(reflect.TypeOf((*error)(nil)).Elem()) { + return cfg.error("init in %s, func return invalid result, error required but return %s", rev.Type().String(), fn.Type().Out(0).String()) + } + + result := fn.Call([]reflect.Value{}) + if result[0].IsNil() { + return nil + } else { + return cfg.error("init in %s, result: %s", rev.Type().String(), result[0].Interface().(error).Error()) + } + } + + typs := rev.Type() + + for i := 0; i < typs.NumField(); i++ { + field := typs.Field(i) + // 这里是设置INIT + ref := rev.FieldByName(field.Name) + + // 在配置本身上面找 + if ref.Kind() == reflect.Struct { + if err := cfg.init(ref); err != nil { + return err + } + } else if ref.Kind() == reflect.Ptr && ref.Elem().Kind() == reflect.Struct { + if err := cfg.init(ref); err != nil { + return err + } + } + + if init := field.Tag.Get("init"); init != "" { + // 获取函数 + method := cfg.fixedField(init) + found := false + fn := rev.MethodByName(method) + if fn.IsValid() && fn.Kind() == reflect.Func { + found = true + } + + if !found && rev.CanAddr() { + fn = rev.Addr().MethodByName(method) + if !fn.IsValid() || fn.Kind() != reflect.Func { + return cfg.error("tag: init:\"%s\" in %s, func not exists", init, rev.Type().String()) + } + } + + if fn.Type().NumOut() != 1 { + return cfg.error("tag: init:\"%s\" in %s,invalid val kind, one result required, but %s return", init, field.Name, ref.Type().String(), fn.Type().NumOut()) + } + + result := fn.Call([]reflect.Value{}) + ref.Set(result[0]) + } + + } + return nil +} + +func (cfg *Config) inSearchKey() { + cfg.searchVal = false + cfg.searchKey = true + cfg.canSkip = true +} + +func (cfg *Config) inSearchVal() { + cfg.searchKey = false + cfg.searchVal = true + cfg.canSkip = false +} + +func (cfg *Config) delimiter(b byte) bool { + return unicode.IsSpace(rune(b)) +} + +func (cfg *Config) clearQuoted(s string) string { + s = strings.TrimSpace(s) + if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') { + return s[1 : len(s)-1] + } + + return s +} + +func (cfg *Config) splitQuoted(s string) ([]string, error) { + var sq []string + s = strings.TrimSpace(s) + var last_space bool = true + var need_space bool = true + var d_quote bool = false + var s_quote bool = false + var quote bool = false + var ch byte + var vs bytes.Buffer + + for i := 0; i < len(s); i++ { + ch = s[i] + + if quote { + quote = false + vs.WriteByte(ch) + continue + } + + if ch == '\\' { + quote = true + last_space = false + continue + } + + if last_space { + last_space = false + switch ch { + case '"': + d_quote = true + need_space = false + continue + case '\'': + s_quote = true + need_space = false + continue + case ' ': + last_space = true + continue + } + vs.WriteByte(ch) + } else { + if need_space && cfg.delimiter(ch) { + if vs.Len() > 0 { + sq = append(sq, vs.String()) + } + vs.Reset() + last_space = true + continue + } + + if d_quote { + if ch == '"' { + d_quote = false + need_space = true + continue + } + } else if s_quote { + if ch == '\'' { + s_quote = false + need_space = true + continue + } + } + + vs.WriteByte(ch) + } + } + + if quote || s_quote || d_quote { + return nil, cfg.error(fmt.Sprintf("invalid value: %v", s)) + } + + if vs.Len() > 0 { + sq = append(sq, vs.String()) + } + + return sq, nil +} diff --git a/hook.go b/hook.go new file mode 100644 index 0000000..5363b3f --- /dev/null +++ b/hook.go @@ -0,0 +1,31 @@ +package config + +import "reflect" + +type hook struct { + *Config +} + +func (hk *hook) Unique(s string) error { + parent, b := hk.getNearestSlice() + if !b { + return hk.error("can't not hook unique on \"%s\" directive, unique must on slice with struct", hk.typ.Name) + } + + if parent.Kind() != reflect.Slice { + return hk.error("unique only can be hook in struct of slice") + } + + // last is current + for i := 0; i < parent.Len()-1; i++ { + cur := parent.Index(i).FieldByName(hk.typ.Name) + if !cur.CanInterface() { + return hk.error("can't be interface at parent") + } + if cur.Interface() == hk.current.Interface() { + return hk.error("duplication value \"%s\" for %s", s, hk.typ.Name) + } + } + + return nil +} diff --git a/stash.go b/stash.go new file mode 100644 index 0000000..8e339b1 --- /dev/null +++ b/stash.go @@ -0,0 +1,88 @@ +package config + +import ( + "os" + "reflect" +) + +type stash struct { + file *os.File + line int64 + searchVal bool + searchKey bool + inBlock int + canSkip bool + skip bool + bkQueue []bool + bkMulti bool + mapKey reflect.Value + setVar bool + searchVar bool + searchVarBlock bool + cwd string + vars []map[string]string +} + +func (cfg *Config) popStash() error { + if cfg.inBlock > 0 { + return cfg.error("invalid config file, block not closed by \"}\"") + } + + if len(cfg.stash) > 0 { + current := cfg.stash[len(cfg.stash)-1] + cfg.stash = cfg.stash[:len(cfg.stash)-1] + cfg.file = current.file + cfg.line = current.line + cfg.searchVal = current.searchVal + cfg.searchKey = current.searchKey + cfg.inBlock = current.inBlock + cfg.canSkip = current.canSkip + cfg.skip = current.skip + cfg.bkMulti = current.bkMulti + cfg.mapKey = current.mapKey + cfg.setVar = current.setVar + cfg.searchVar = current.searchVar + cfg.searchVarBlock = current.searchVarBlock + cfg.cwd = current.cwd + + cfg.bkQueue = make([]bool, len(current.bkQueue)) + cfg.vars = make([]map[string]string, len(current.vars)) + copy(cfg.bkQueue, current.bkQueue) + copy(cfg.vars, current.vars) + } + + return nil +} + +func (cfg *Config) pushStash() { + s := &stash{ + file: cfg.file, + line: cfg.line, + searchVal: cfg.searchVal, + searchKey: cfg.searchKey, + inBlock: cfg.inBlock, + canSkip: cfg.canSkip, + skip: cfg.skip, + bkMulti: cfg.bkMulti, + mapKey: cfg.mapKey, + setVar: cfg.setVar, + searchVar: cfg.searchVar, + searchVarBlock: cfg.searchVarBlock, + cwd: cfg.cwd, + } + + s.bkQueue = make([]bool, len(cfg.bkQueue)) + s.vars = make([]map[string]string, len(cfg.vars)) + copy(s.bkQueue, cfg.bkQueue) + copy(s.vars, cfg.vars) + + cfg.inSearchKey() + cfg.inBlock = 0 + cfg.inInclude = 0 + cfg.currentVar = make(map[string]string) + cfg.searchVar = false + cfg.setVar = false + cfg.searchVarBlock = false + + cfg.stash = append(cfg.stash, s) +} diff --git a/value.go b/value.go new file mode 100644 index 0000000..f39f647 --- /dev/null +++ b/value.go @@ -0,0 +1,243 @@ +package config + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" + "time" +) + +func (cfg *Config) set(s string) error { + s = strings.TrimSpace(s) + if cfg.current.Kind() == reflect.Ptr { + if cfg.current.IsNil() { + cfg.current.Set(reflect.New(cfg.current.Type().Elem())) + } + cfg.current = cfg.current.Elem() + } + + // 这里判定一下,是不是要格式化数值 + if format := cfg.typ.Tag.Get("format"); format != "" { + if err := cfg.setByFormat(format, s); err != nil { + return err + } + } else { + if err := cfg.setByRaw(s); err != nil { + return err + } + } + + // hook check + if hook := cfg.typ.Tag.Get("hook"); hook != "" { + return cfg.runHook(hook, s) + } + + return nil +} + +func (cfg *Config) setByRaw(s string) error { + switch cfg.current.Kind() { + case reflect.String: + cfg.current.SetString(cfg.clearQuoted(s)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if cfg.current.Type().String() == "time.Duration" { + if time, err := time.ParseDuration(s); err != nil { + return cfg.error(err.Error()) + } else { + cfg.current.Set(reflect.ValueOf(time)) + } + } else { + itmp, err := strconv.ParseInt(s, 10, cfg.current.Type().Bits()) + if err != nil { + return cfg.error(err.Error()) + } + if !cfg.current.OverflowInt(itmp) { + cfg.current.SetInt(itmp) + } else { + return cfg.error("value overflow") + } + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + itmp, err := strconv.ParseUint(s, 10, cfg.current.Type().Bits()) + if err != nil { + return cfg.error(err.Error()) + } + if !cfg.current.OverflowUint(itmp) { + cfg.current.SetUint(itmp) + } else { + return cfg.error("value overflow") + } + case reflect.Float32, reflect.Float64: + ftmp, err := strconv.ParseFloat(s, cfg.current.Type().Bits()) + if err != nil { + return cfg.error(err.Error()) + } + if !cfg.current.OverflowFloat(ftmp) { + cfg.current.SetFloat(ftmp) + } else { + return cfg.error("value overflow") + } + case reflect.Bool: + if s == "yes" || s == "on" { + cfg.current.SetBool(true) + } else if s == "no" || s == "off" { + cfg.current.SetBool(false) + } else { + btmp, err := strconv.ParseBool(s) + if err != nil { + return cfg.error(err.Error()) + } + cfg.current.SetBool(btmp) + } + case reflect.Slice: + sf, err := cfg.splitQuoted(s) + if err != nil { + return err + } + for _, sv := range sf { + n := cfg.current.Len() + ref := reflect.Zero(cfg.current.Type().Elem()) + cfg.init(ref) + cfg.current.Set(reflect.Append(cfg.current, ref)) + cfg.pushElement(cfg.current.Index(n)) + cfg.set(sv) + cfg.popElement() + } + case reflect.Map: + if cfg.current.IsNil() { + cfg.current.Set(reflect.MakeMap(cfg.current.Type())) + } + + sf, err := cfg.splitQuoted(s) + if err != nil { + return err + } + if len(sf) != 2 { + return cfg.error("invalid map config: %s", s) + } + var v reflect.Value + v = reflect.New(cfg.current.Type().Key()) + cfg.pushElement(v) + cfg.set(sf[0]) + key := cfg.current + cfg.popElement() + v = reflect.New(cfg.current.Type().Elem()) + cfg.pushElement(v) + cfg.set(sf[1]) + val := cfg.current + cfg.popElement() + + cfg.current.SetMapIndex(key, val) + default: + if cfg.current.Kind() == reflect.Struct { + return cfg.error("invalid block, start a block with '{'") + } else { + return cfg.error(fmt.Sprintf("invalid type:%s", cfg.current.Kind())) + } + } + + return nil +} + +func (cfg *Config) setByFormat(format, s string) error { + var fn reflect.Value + found := false + method := cfg.fixedField(format) + // 在配置本身上面找 + if element, ok := cfg.getStruct(); ok { + if element.Kind() != reflect.Ptr && element.CanAddr() { + element = element.Addr() + } + + fn = element.MethodByName(method) + found = fn.IsValid() && fn.Kind() == reflect.Func + } + + if !found { + fn = reflect.ValueOf(cfg.format).MethodByName(method) + + if !fn.IsValid() || fn.Kind() != reflect.Func { + return cfg.error("tag: fomrat:\"%s\" in %s, func not exists", format, cfg.typ.Name) + } + } + + if fn.Type().NumOut() != 2 { + return cfg.error("tag: fomrat:\"%s\" in %s, func return invalid result, 2 result required", format, cfg.typ.Name) + } + + if fn.Type().Out(0) != cfg.current.Type() { + return cfg.error("tag: fomrat:\"%s\" in %s, invalid val kind, result is %s, but %s required", format, cfg.typ.Name, fn.Type().Out(0).String(), cfg.current.Type().String()) + } + + if !fn.Type().Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) { + return cfg.error("tag: fomrat:\"%s\" in %s, func return invalid result, error type required", format, cfg.typ.Name) + } + + result := fn.Call([]reflect.Value{reflect.ValueOf(s)}) + + if !result[1].IsNil() { + return cfg.error("tag: fomrat:\"%s\" in %s, invalid val with %s, and return %s", format, cfg.typ.Name, s, result[1].Interface().(error).Error()) + } + + cfg.current.Set(result[0]) + return nil +} + +func (cfg *Config) replace(s *bytes.Buffer, vs *bytes.Buffer) bool { + if vs.Len() == 0 { + return false + } + + for k, v := range cfg.currentVar { + if strings.Compare(k, vs.String()) == 0 { + // found + cfg.searchVar = false + s.WriteString(v) + vs.Reset() + return true + } + } + + for i := len(cfg.vars) - 1; i >= 0; i-- { + for k, v := range cfg.vars[i] { + if strings.Compare(k, vs.String()) == 0 { + s.WriteString(v) + cfg.searchVar = false + vs.Reset() + return true + } + } + } + + return false +} + +func (cfg *Config) runHook(hook string, s string) error { + hook = cfg.fixedField(hook) + fn, found := cfg.getMethod(hook) + + if !found { + fn = reflect.ValueOf(cfg.hook).MethodByName(hook) + if !fn.IsValid() || fn.Kind() != reflect.Func { + return cfg.error("tag: hook:\"%s\" in %s, func not exists", hook, cfg.typ.Name) + } + } + + if fn.Type().NumOut() != 1 { + return cfg.error("tag: hook:\"%s\" in %s, func return invalid result", hook, cfg.typ.Name) + } + + if !fn.Type().Out(0).Implements(reflect.TypeOf((*error)(nil)).Elem()) { + return cfg.error("tag: hook:\"%s\" in %s, func return invalid result, error required", hook, cfg.typ.Name) + } + + result := fn.Call([]reflect.Value{reflect.ValueOf(s)}) + + if !result[0].IsNil() { + return result[0].Interface().(error) + } + + return nil +}