Repository: lytics/confl Branch: master Commit: 08c6aed5f53f Files: 26 Total size: 126.8 KB Directory structure: gitextract_t1zcxjnh/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _examples/ │ ├── example.conf │ └── example.go ├── cmd/ │ └── conflv/ │ ├── COPYING │ ├── README.md │ └── main.go ├── codecov.yml ├── decode.go ├── decode_meta.go ├── decode_test.go ├── doc.go ├── encode.go ├── encode_test.go ├── encoding_types.go ├── encoding_types_1.1.go ├── fuzz.go ├── go.test.sh ├── lex.go ├── lex_test.go ├── parse.go ├── parse_test.go ├── type_check.go └── type_fields.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ TAGS tags .*.swp /confl-fuzz.zip /fuzz ================================================ FILE: .travis.yml ================================================ language: go go: - 1.9.x - 1.10.x before_install: - go get -t -v ./... script: - bash go.test.sh after_success: - bash <(curl -s https://codecov.io/bash) ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2012-2013 Apcera Inc Copyright (c) 2014 Lytics Inc. Copyright (c) 2013-2014 https://github.com/BurntSushi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ## Yet another Config Parser for go This is a config parser most similar to Nginx, supports Json format, but with line-breaks, comments, etc. Also, like Nginx is more lenient. Uses same syntax as https://github.com/vstakhov/libucl [![Code Coverage](https://codecov.io/gh/lytics/confl/branch/master/graph/badge.svg)](https://codecov.io/gh/lytics/confl) [![GoDoc](https://godoc.org/github.com/lytics/confl?status.svg)](http://godoc.org/github.com/lytics/confl) [![Build Status](https://travis-ci.org/lytics/confl.svg?branch=master)](https://travis-ci.org/lytics/confl) [![Go ReportCard](https://goreportcard.com/badge/lytics/confl)](https://goreportcard.com/report/lytics/confl) Use [SublimeText Nginx Plugin](https://github.com/brandonwamboldt/sublime-nginx) for formatting. Credit to [BurntSushi/Toml](https://github.com/BurntSushi/toml) and [Apcera/Gnatsd](https://github.com/apcera/gnatsd/tree/master/conf) from which this was derived. ### Other Options There are a variety of options for config with comments in Go, this project started before some of them which are now probably better options: * https://github.com/hjson/hjson-go * Similar to above? https://github.com/tailscale/hujson * https://github.com/lalamove/konfig * https://github.com/hashicorp/hcl ### Example ``` # nice, a config with comments! # support the name = value format title = "conf Example" # support json semicolon title2 : "conf example2" # support omitting = or : because key starts a line title3 "conf example" # note, we do not have to have quotes title4 = Without Quotes # for Sections we can use brackets hand { name = "Tyrion" organization = "Lannisters" bio = "Imp" // comments on fields dob = 1979-05-27T07:32:00Z # dates, and more comments on fields } // Note, double-slash comment // section name/value that is quoted and json valid, including commas address : { "street" : "1 Sky Cell", "city" : "Eyre", "region" : "Vale of Arryn", "country" : "Westeros" } # sections can omit the colon, equal before bracket seenwith { # nested section # can be spaces or tabs for nesting jaime : { season = season1 episode = "episode1" } cersei = { season = season1 episode = "episode1" } } # Line breaks are OK when inside arrays seasons = [ "season1", "season2", "season3", "season4", "???" ] # long strings can use parens to allow multi-line description ( we possibly can have multi line text with a block paren block ends with end paren on new line ) ``` And the corresponding Go types are: ```go type Config struct { Title string Hand HandOfKing Location *Address `confl:"address"` Seenwith map[string]Character Seasons []string Description string } type HandOfKing struct { Name string Org string `json:"organization"` // Reads either confl, or json attributes Bio string DOB time.Time Deceased bool } type Address struct { Street string City string Region string ZipCode int } type Character struct { Episode string Season string } ``` Note that a case insensitive match will be tried if an exact match can't be found. A working example of the above can be found in `_examples/example.{go,conf}`. ### Examples This package works similarly to how the Go standard library handles `XML` and `JSON`. Namely, data is loaded into Go values via reflection. For the simplest example, consider a file as just a list of keys and values: ``` // Comments in Config Age = 25 # another comment Cats = [ "Cauchy", "Plato" ] # now, using quotes on key "Pi" = 3.14 Perfection = [ 6, 28, 496, 8128 ] DOB = 1987-07-05T05:45:00Z ``` Which could be defined in Go as: ```go type Config struct { Age int Cats []string Pi float64 Perfection []int DOB time.Time } ``` And then decoded with: ```go var conf Config if err := confl.Unmarshal(byteData, &conf); err != nil { // handle error } ``` You can also use struct tags if your struct field name doesn't map to a confl key value directly: ``` some_key_NAME = "wat" ``` ```go type Config struct { ObscureKey string `confl:"some_key_NAME"` } ``` ### Using the `encoding.TextUnmarshaler` interface Here's an example that automatically parses duration strings into `time.Duration` values: ``` song [ { name = "Thunder Road" duration = "4m49s" }, { name = "Stairway to Heaven" duration = "8m03s" } ] ``` Which can be decoded with: ```go type song struct { Name string Duration duration } type songs struct { Song []song } var favorites songs if err := confl.Unmarshal(blob, &favorites); err != nil { log.Fatal(err) } for _, s := range favorites.Song { fmt.Printf("%s (%s)\n", s.Name, s.Duration) } ``` And you'll also need a `duration` type that satisfies the `encoding.TextUnmarshaler` interface: ```go type duration struct { time.Duration } func (d *duration) UnmarshalText(text []byte) error { var err error d.Duration, err = time.ParseDuration(string(text)) return err } ``` ================================================ FILE: _examples/example.conf ================================================ # Nice, we have comments title = "Confl Example" hand { name = "Tyrion" organization = "Lannisters" bio = "Imp" // comments on fields dob = 1979-05-27T07:32:00Z # dates, and more comments on fields } // Now, something that is json esque (but with comments) address : { "street": "1 Sky Cell", // the street presumably "city": "Eyre", // not always here but.... "region": "Vale of Arryn", "country": "Westeros" } seenwith { # You can indent as you please. Tabs or spaces jaime { season = season1 episode = "episode1" } cersei { season = season1 episode = "episode1" } } # Line breaks are OK when inside arrays seasons = [ "season1", "season2", "season3", "season4", "???" ] description ( we possibly can have multi line text with a block paren block ends with end paren on new line ) ================================================ FILE: _examples/example.go ================================================ package main import ( "fmt" "time" "github.com/araddon/gou" "github.com/lytics/confl" ) type Config struct { Title string Hand handOfKing Location address `confl:"address"` Seenwith map[string]character Seasons []string Description string } /* hand { name = "Tyrion" organization = "Lannisters" bio = "Imp" // comments on fields dob = 1979-05-27T07:32:00Z # dates, and more comments on fields } */ type handOfKing struct { Name string Org string `confl:"organization"` Bio string DOB time.Time Deceased bool } type address struct { Street string City string Region string ZipCode int } type character struct { Episode string Season string } func main() { gou.SetupLogging("debug") gou.SetColorOutput() var config Config if _, err := confl.DecodeFile("example.conf", &config); err != nil { fmt.Println(err) return } fmt.Printf("Title: %s\n", config.Title) fmt.Printf("Hand: %s %s, %s. Born: %s, Deceased? %v\n", config.Hand.Name, config.Hand.Org, config.Hand.Bio, config.Hand.DOB, config.Hand.Deceased) fmt.Printf("Location: %#v\n", config.Location) for name, person := range config.Seenwith { fmt.Printf("Seen With: %s (%s, %s)\n", name, person.Episode, person.Season) } fmt.Printf("Seasons: %v\n", config.Seasons) fmt.Printf("Description: %v\n", config.Description) } ================================================ FILE: cmd/conflv/COPYING ================================================ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2004 Sam Hocevar Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. ================================================ FILE: cmd/conflv/README.md ================================================ # CONFL Validator If Go is installed, it's simple to try it out: ```bash go get github.com/lytics/confl/cmd/conflv conflv myconf.conf ``` You can see the types of every key in a conf file with: ```bash conflv -types the.conf ``` ================================================ FILE: cmd/conflv/main.go ================================================ package main import ( "flag" "fmt" "log" "os" "path" "strings" "text/tabwriter" "github.com/lytics/confl" ) var ( flagTypes = false ) func init() { log.SetFlags(0) flag.BoolVar(&flagTypes, "types", flagTypes, "When set, the types of every defined key will be shown.") flag.Usage = usage flag.Parse() } func usage() { log.Printf("Usage: %s name.conf [ file2 ... ]\n", path.Base(os.Args[0])) flag.PrintDefaults() os.Exit(1) } func main() { if flag.NArg() < 1 { flag.Usage() } for _, f := range flag.Args() { var tmp interface{} md, err := confl.DecodeFile(f, &tmp) if err != nil { log.Fatalf("Error in '%s': %s", f, err) } if flagTypes { printTypes(md) } } } func printTypes(md confl.MetaData) { tabw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) for _, key := range md.Keys() { fmt.Fprintf(tabw, "%s%s\t%s\n", strings.Repeat(" ", len(key)-1), key, md.Type(key...)) } tabw.Flush() } ================================================ FILE: codecov.yml ================================================ ignore: - "cmd/*" - "_examples/*" ================================================ FILE: decode.go ================================================ package confl import ( "fmt" u "github.com/araddon/gou" "io" "io/ioutil" "math" "reflect" "strings" "time" ) var _ = u.EMPTY var e = fmt.Errorf // Primitive is a value that hasn't been decoded into a Go value. // When using the various `Decode*` functions, the type `Primitive` may // be given to any value, and its decoding will be delayed. // // A `Primitive` value can be decoded using the `PrimitiveDecode` function. // // The underlying representation of a `Primitive` value is subject to change. // Do not rely on it. // // N.B. Primitive values are still parsed, so using them will only avoid // the overhead of reflection. They can be useful when you don't know the // exact type of data until run time. type Primitive struct { undecoded interface{} context Key } // PrimitiveDecode is just like the other `Decode*` functions, except it // decodes a confl value that has already been parsed. Valid primitive values // can *only* be obtained from values filled by the decoder functions, // including this method. (i.e., `v` may contain more `Primitive` // values.) // // Meta data for primitive values is included in the meta data returned by // the `Decode*` functions with one exception: keys returned by the Undecoded // method will only reflect keys that were decoded. Namely, any keys hidden // behind a Primitive will be considered undecoded. Executing this method will // update the undecoded keys in the meta data. (See the example.) func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error { md.context = primValue.context defer func() { md.context = nil }() return md.unify(primValue.undecoded, rvalue(v)) } type Decoder struct { reader io.Reader } func NewDecoder(r io.Reader) *Decoder { return &Decoder{r} } func (dec *Decoder) Decode(v interface{}) error { bs, err := ioutil.ReadAll(dec.reader) if err != nil { return err } _, err = Decode(string(bs), v) return err } func Unmarshal(bs []byte, v interface{}) error { _, err := Decode(string(bs), v) return err } // Decode will decode the contents of `data` in confl format into a pointer // `v`. // // confl hashes correspond to Go structs or maps. (Dealer's choice. They can be // used interchangeably.) // // confl arrays of tables correspond to either a slice of structs or a slice // of maps. // // confl datetimes correspond to Go `time.Time` values. // // All other confl types (float, string, int, bool and array) correspond // to the obvious Go types. // // An exception to the above rules is if a type implements the // encoding.TextUnmarshaler interface. In this case, any primitive confl value // (floats, strings, integers, booleans and datetimes) will be converted to // a byte string and given to the value's UnmarshalText method. See the // Unmarshaler example for a demonstration with time duration strings. // // Key mapping // // confl keys can map to either keys in a Go map or field names in a Go // struct. The special `confl` struct tag may be used to map confl keys to // struct fields that don't match the key name exactly. (See the example.) // A case insensitive match to struct names will be tried if an exact match // can't be found. // // The mapping between confl values and Go values is loose. That is, there // may exist confl values that cannot be placed into your representation, and // there may be parts of your representation that do not correspond to // confl values. This loose mapping can be made stricter by using the IsDefined // and/or Undecoded methods on the MetaData returned. // // This decoder will not handle cyclic types. If a cyclic type is passed, // `Decode` will not terminate. func Decode(data string, v interface{}) (MetaData, error) { p, err := parse(data) if err != nil { return MetaData{}, err } md := MetaData{ p.mapping, p.types, p.ordered, make(map[string]bool, len(p.ordered)), nil, } return md, md.unify(p.mapping, rvalue(v)) } // DecodeFile is just like Decode, except it will automatically read the // contents of the file at `fpath` and decode it for you. func DecodeFile(fpath string, v interface{}) (MetaData, error) { bs, err := ioutil.ReadFile(fpath) if err != nil { return MetaData{}, err } return Decode(string(bs), v) } // DecodeReader is just like Decode, except it will consume all bytes // from the reader and decode it for you. func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { bs, err := ioutil.ReadAll(r) if err != nil { return MetaData{}, err } return Decode(string(bs), v) } // unify performs a sort of type unification based on the structure of `rv`, // which is the client representation. // // Any type mismatch produces an error. Finding a type that we don't know // how to handle produces an unsupported type error. func (md *MetaData) unify(data interface{}, rv reflect.Value) error { // Special case. Look for a `Primitive` value. if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() { // Save the undecoded data and the key context into the primitive // value. context := make(Key, len(md.context)) copy(context, md.context) rv.Set(reflect.ValueOf(Primitive{ undecoded: data, context: context, })) return nil } // Special case. Handle time.Time values specifically. // TODO: Remove this code when we decide to drop support for Go 1.1. // This isn't necessary in Go 1.2 because time.Time satisfies the encoding // interfaces. if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) { return md.unifyDatetime(data, rv) } // Special case. Look for a value satisfying the TextUnmarshaler interface. if v, ok := rv.Interface().(TextUnmarshaler); ok { return md.unifyText(data, v) } // BUG(burntsushi) // The behavior here is incorrect whenever a Go type satisfies the // encoding.TextUnmarshaler interface but also corresponds to a conf // hash or array. In particular, the unmarshaler should only be applied // to primitive conf values. But at this point, it will be applied to // all kinds of values and produce an incorrect error whenever those values // are hashes or arrays (including arrays of tables). k := rv.Kind() // laziness if k >= reflect.Int && k <= reflect.Uint64 { return md.unifyInt(data, rv) } switch k { case reflect.Ptr: elem := reflect.New(rv.Type().Elem()) err := md.unify(data, reflect.Indirect(elem)) if err != nil { return err } rv.Set(elem) return nil case reflect.Struct: return md.unifyStruct(data, rv) case reflect.Map: return md.unifyMap(data, rv) case reflect.Array: return md.unifyArray(data, rv) case reflect.Slice: return md.unifySlice(data, rv) case reflect.String: return md.unifyString(data, rv) case reflect.Bool: return md.unifyBool(data, rv) case reflect.Interface: // we only support empty interfaces. if rv.NumMethod() > 0 { return e("Unsupported type '%s'.", rv.Kind()) } return md.unifyAnything(data, rv) case reflect.Float32: fallthrough case reflect.Float64: return md.unifyFloat64(data, rv) } return e("Unsupported type '%s'.", rv.Kind()) } func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error { tmap, ok := mapping.(map[string]interface{}) if !ok { return mismatch(rv, "map", mapping) } for key, datum := range tmap { var f *field fields := cachedTypeFields(rv.Type()) for i := range fields { ff := &fields[i] if ff.name == key { f = ff break } if f == nil && strings.EqualFold(ff.name, key) { f = ff } } if f != nil { subv := rv for _, i := range f.index { subv = indirect(subv.Field(i)) } if isUnifiable(subv) { md.decoded[md.context.add(key).String()] = true md.context = append(md.context, key) if err := md.unify(datum, subv); err != nil { return e("Type mismatch for '%s.%s': %s", rv.Type().String(), f.name, err) } md.context = md.context[0 : len(md.context)-1] } else if f.name != "" { // Bad user! No soup for you! return e("Field '%s.%s' is unexported, and therefore cannot "+ "be loaded with reflection.", rv.Type().String(), f.name) } } } return nil } func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error { tmap, ok := mapping.(map[string]interface{}) if !ok { return badtype("map", mapping) } if rv.IsNil() { rv.Set(reflect.MakeMap(rv.Type())) } for k, v := range tmap { md.decoded[md.context.add(k).String()] = true md.context = append(md.context, k) rvkey := indirect(reflect.New(rv.Type().Key())) rvval := reflect.Indirect(reflect.New(rv.Type().Elem())) if err := md.unify(v, rvval); err != nil { return err } md.context = md.context[0 : len(md.context)-1] rvkey.SetString(k) rv.SetMapIndex(rvkey, rvval) } return nil } func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error { datav := reflect.ValueOf(data) if datav.Kind() != reflect.Slice { return badtype("slice", data) } sliceLen := datav.Len() if sliceLen != rv.Len() { return e("expected array length %d; got array of length %d", rv.Len(), sliceLen) } return md.unifySliceArray(datav, rv) } func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error { datav := reflect.ValueOf(data) if datav.Kind() != reflect.Slice { return badtype("slice", data) } sliceLen := datav.Len() if rv.IsNil() { rv.Set(reflect.MakeSlice(rv.Type(), sliceLen, sliceLen)) } return md.unifySliceArray(datav, rv) } func (md *MetaData) unifySliceArray(data, rv reflect.Value) error { sliceLen := data.Len() for i := 0; i < sliceLen; i++ { v := data.Index(i).Interface() sliceval := indirect(rv.Index(i)) if err := md.unify(v, sliceval); err != nil { return err } } return nil } func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error { if _, ok := data.(time.Time); ok { rv.Set(reflect.ValueOf(data)) return nil } return badtype("time.Time", data) } func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error { if s, ok := data.(string); ok { rv.SetString(s) return nil } return badtype("string", data) } func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error { if num, ok := data.(float64); ok { switch rv.Kind() { case reflect.Float32: fallthrough case reflect.Float64: rv.SetFloat(num) default: panic("bug") } return nil } return badtype("float", data) } func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error { if num, ok := data.(int64); ok { if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 { switch rv.Kind() { case reflect.Int, reflect.Int64: // No bounds checking necessary. case reflect.Int8: if num < math.MinInt8 || num > math.MaxInt8 { return e("Value '%d' is out of range for int8.", num) } case reflect.Int16: if num < math.MinInt16 || num > math.MaxInt16 { return e("Value '%d' is out of range for int16.", num) } case reflect.Int32: if num < math.MinInt32 || num > math.MaxInt32 { return e("Value '%d' is out of range for int32.", num) } } rv.SetInt(num) } else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 { unum := uint64(num) switch rv.Kind() { case reflect.Uint, reflect.Uint64: // No bounds checking necessary. case reflect.Uint8: if num < 0 || unum > math.MaxUint8 { return e("Value '%d' is out of range for uint8.", num) } case reflect.Uint16: if num < 0 || unum > math.MaxUint16 { return e("Value '%d' is out of range for uint16.", num) } case reflect.Uint32: if num < 0 || unum > math.MaxUint32 { return e("Value '%d' is out of range for uint32.", num) } } rv.SetUint(unum) } else { panic("unreachable") } return nil } return badtype("integer", data) } func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error { if b, ok := data.(bool); ok { rv.SetBool(b) return nil } return badtype("boolean", data) } func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error { rv.Set(reflect.ValueOf(data)) return nil } func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error { var s string switch sdata := data.(type) { case TextMarshaler: text, err := sdata.MarshalText() if err != nil { return err } s = string(text) case fmt.Stringer: s = sdata.String() case string: s = sdata case bool: s = fmt.Sprintf("%v", sdata) case int64: s = fmt.Sprintf("%d", sdata) case float64: s = fmt.Sprintf("%f", sdata) default: return badtype("primitive (string-like)", data) } if err := v.UnmarshalText([]byte(s)); err != nil { return err } return nil } // rvalue returns a reflect.Value of `v`. All pointers are resolved. func rvalue(v interface{}) reflect.Value { return indirect(reflect.ValueOf(v)) } // indirect returns the value pointed to by a pointer. // Pointers are followed until the value is not a pointer. // New values are allocated for each nil pointer. // // An exception to this rule is if the value satisfies an interface of // interest to us (like encoding.TextUnmarshaler). func indirect(v reflect.Value) reflect.Value { if v.Kind() != reflect.Ptr { if v.CanAddr() { pv := v.Addr() if _, ok := pv.Interface().(TextUnmarshaler); ok { return pv } } return v } if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } return indirect(reflect.Indirect(v)) } func isUnifiable(rv reflect.Value) bool { if rv.CanSet() { return true } if _, ok := rv.Interface().(TextUnmarshaler); ok { return true } return false } func badtype(expected string, data interface{}) error { return e("Expected %s but found '%T'.", expected, data) } func mismatch(user reflect.Value, expected string, data interface{}) error { return e("Type mismatch for %s. Expected %s but found '%T'.", user.Type().String(), expected, data) } ================================================ FILE: decode_meta.go ================================================ package confl import "strings" // MetaData allows access to meta information about data that may not // be inferrable via reflection. In particular, whether a key has been defined // and the type of a key. type MetaData struct { mapping map[string]interface{} types map[string]confType keys []Key decoded map[string]bool context Key // Used only during decoding. } // IsDefined returns true if the key given exists in the data. The key // should be specified hierarchially. e.g., // // // access the key 'a.b.c' // IsDefined("a", "b", "c") // // IsDefined will return false if an empty key given. Keys are case sensitive. func (md *MetaData) IsDefined(key ...string) bool { if len(key) == 0 { return false } var hash map[string]interface{} var ok bool var hashOrVal interface{} = md.mapping for _, k := range key { if hash, ok = hashOrVal.(map[string]interface{}); !ok { return false } if hashOrVal, ok = hash[k]; !ok { return false } } return true } // Type returns a string representation of the type of the key specified. // // Type will return the empty string if given an empty key or a key that // does not exist. Keys are case sensitive. func (md *MetaData) Type(key ...string) string { fullkey := strings.Join(key, ".") if typ, ok := md.types[fullkey]; ok { return typ.typeString() } return "" } // Key is the type of any key, including key groups. Use (MetaData).Keys // to get values of this type. type Key []string func (k Key) String() string { return strings.Join(k, ".") } func (k Key) add(piece string) Key { newKey := make(Key, len(k)+1) copy(newKey, k) newKey[len(k)] = piece return newKey } func (k Key) insert(piece string) Key { newKey := make(Key, len(k), len(k)+1) copy(newKey, k) insertedKey := make(Key, 1) insertedKey[0] = piece newKey = append(insertedKey, newKey...) return newKey } // Keys returns a slice of every key in the data, including key groups. // Each key is itself a slice, where the first element is the top of the // hierarchy and the last is the most specific. // // The list will have the same order as the keys appeared in the data. // // All keys returned are non-empty. func (md *MetaData) Keys() []Key { return md.keys } // Undecoded returns all keys that have not been decoded in the order in which // they appear in the original document. // // This includes keys that haven't been decoded because of a Primitive value. // Once the Primitive value is decoded, the keys will be considered decoded. // // Also note that decoding into an empty interface will result in no decoding, // and so no keys will be considered decoded. // // In this sense, the Undecoded keys correspond to keys in the document // that do not have a concrete type in your representation. func (md *MetaData) Undecoded() []Key { undecoded := make([]Key, 0, len(md.keys)) for _, key := range md.keys { if !md.decoded[key.String()] { undecoded = append(undecoded, key) } } return undecoded } ================================================ FILE: decode_test.go ================================================ package confl import ( "flag" "fmt" "log" "reflect" "strings" "testing" "time" u "github.com/araddon/gou" "github.com/stretchr/testify/assert" ) func init() { log.SetFlags(0) flag.Parse() if testing.Verbose() { u.SetupLogging("debug") u.SetColorOutput() } } func TestDecodeSimple(t *testing.T) { var simpleConfigString = ` age = 250 ageptr2 = 200 andrew = "gallant" kait = "brady" now = 1987-07-05T05:45:00Z yesOrNo = true pi = 3.14 colors = [ ["red", "green", "blue"], ["cyan", "magenta", "yellow", "black"], [pink,brown], ] my { Cats { plato = "cat 1" cauchy = "cat 2" } } games [ { // more comments behind tab name "game of thrones" # we have a comment sku "got" // another comment, empty line next } { name "settlers of catan" // a comment } ] ` type cats struct { PlatoAlias string `json:"plato,omitempty"` CauchyConflAlias string `confl:"cauchy"` } type game struct { Name string Sku string } type simpleType struct { Age int AgePtr *int AgePtr2 *int Colors [][]string Pi float64 YesOrNo bool Now time.Time Andrew string Kait string My map[string]cats Games []*game } var simple simpleType _, err := Decode(simpleConfigString, &simple) assert.True(t, err == nil, "err nil?%v", err) now, err := time.Parse("2006-01-02T15:04:05", "1987-07-05T05:45:00") if err != nil { panic(err) } age200 := int(200) var answer = simpleType{ Age: 250, AgePtr2: &age200, Andrew: "gallant", Kait: "brady", Now: now, YesOrNo: true, Pi: 3.14, Colors: [][]string{ {"red", "green", "blue"}, {"cyan", "magenta", "yellow", "black"}, {"pink", "brown"}, }, My: map[string]cats{ "Cats": cats{PlatoAlias: "cat 1", CauchyConflAlias: "cat 2"}, }, Games: []*game{ &game{"game of thrones", "got"}, &game{Name: "settlers of catan"}, }, } assert.True(t, simple.AgePtr == nil, "must have nil ptr") if !reflect.DeepEqual(simple, answer) { t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", answer, simple) } // Now Try decoding using Decoder var simpleDec simpleType decoder := NewDecoder(strings.NewReader(simpleConfigString)) err = decoder.Decode(&simpleDec) assert.True(t, err == nil, "err nil?%v", err) assert.True(t, simpleDec.AgePtr == nil, "must have nil ptr") if !reflect.DeepEqual(simpleDec, answer) { t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", answer, simpleDec) } // Now Try decoding using Unmarshal var simple2 simpleType err = Unmarshal([]byte(simpleConfigString), &simple2) assert.True(t, err == nil, "err nil?%v", err) assert.True(t, simple2.AgePtr == nil, "must have nil ptr") if !reflect.DeepEqual(simple2, answer) { t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n", answer, simple2) } } func TestDecodeEmbedded(t *testing.T) { type Dog struct{ Name string } type Age int tests := map[string]struct { input string decodeInto interface{} wantDecoded interface{} }{ "embedded struct": { input: `Name = "milton"`, decodeInto: &struct{ Dog }{}, wantDecoded: &struct{ Dog }{Dog{"milton"}}, }, "embedded non-nil pointer to struct": { input: `Name = "milton"`, decodeInto: &struct{ *Dog }{}, wantDecoded: &struct{ *Dog }{&Dog{"milton"}}, }, "embedded nil pointer to struct": { input: ``, decodeInto: &struct{ *Dog }{}, wantDecoded: &struct{ *Dog }{nil}, }, "embedded int": { input: `Age = -5`, decodeInto: &struct{ Age }{}, wantDecoded: &struct{ Age }{-5}, }, } for label, test := range tests { _, err := Decode(test.input, test.decodeInto) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(test.wantDecoded, test.decodeInto) { t.Errorf("%s: want decoded == %+v, got %+v", label, test.wantDecoded, test.decodeInto) } } } func TestDecodeTableArrays(t *testing.T) { var tableArrays = ` albums [ { name = "Born to Run" songs [ { name = "Jungleland" }, { name = "Meeting Across the River" } ] } { name = "Born in the USA" songs [ { name = "Glory Days" }, { name = "Dancing in the Dark" } ] } ]` type Song struct { Name string } type Album struct { Name string Songs []Song } type Music struct { Albums []Album } expected := Music{[]Album{ {"Born to Run", []Song{{"Jungleland"}, {"Meeting Across the River"}}}, {"Born in the USA", []Song{{"Glory Days"}, {"Dancing in the Dark"}}}, }} var got Music if _, err := Decode(tableArrays, &got); err != nil { t.Fatal(err) } if !reflect.DeepEqual(expected, got) { t.Fatalf("\n%#v\n!=\n%#v\n", expected, got) } } // Case insensitive matching tests. // A bit more comprehensive than needed given the current implementation, // but implementations change. // Probably still missing demonstrations of some ugly corner cases regarding // case insensitive matching and multiple fields. func TestDecodeCase(t *testing.T) { var caseData = ` tOpString = "string" tOpInt = 1 tOpFloat = 1.1 tOpBool = true tOpdate = 2006-01-02T15:04:05Z tOparray = [ "array" ] Match = "i should be in Match only" MatcH = "i should be in MatcH only" once = "just once" nEst { eD { nEstedString = "another string" } } ` type InsensitiveEd struct { NestedString string } type InsensitiveNest struct { Ed InsensitiveEd } type Insensitive struct { TopString string TopInt int TopFloat float64 TopBool bool TopDate time.Time TopArray []string Match string MatcH string Once string OncE string Nest InsensitiveNest } tme, err := time.Parse(time.RFC3339, time.RFC3339[:len(time.RFC3339)-5]) if err != nil { panic(err) } expected := Insensitive{ TopString: "string", TopInt: 1, TopFloat: 1.1, TopBool: true, TopDate: tme, TopArray: []string{"array"}, MatcH: "i should be in MatcH only", Match: "i should be in Match only", Once: "just once", OncE: "", Nest: InsensitiveNest{ Ed: InsensitiveEd{NestedString: "another string"}, }, } var got Insensitive if _, err := Decode(caseData, &got); err != nil { t.Fatal(err) } if !reflect.DeepEqual(expected, got) { t.Fatalf("\n%#v\n!=\n%#v\n", expected, got) } } func TestDecodePointers(t *testing.T) { type Object struct { Type string Description string } type Dict struct { NamedObject map[string]*Object BaseObject *Object Strptr *string Strptrs []*string } s1, s2, s3 := "blah", "abc", "def" expected := &Dict{ Strptr: &s1, Strptrs: []*string{&s2, &s3}, NamedObject: map[string]*Object{ "foo": {"FOO", "fooooo!!!"}, "bar": {"BAR", "ba-ba-ba-ba-barrrr!!!"}, }, BaseObject: &Object{"BASE", "da base"}, } ex1 := ` Strptr = "blah" Strptrs = ["abc", "def"] NamedObject { foo { Type = "FOO" Description = "fooooo!!!" } bar { Type = "BAR" Description = "ba-ba-ba-ba-barrrr!!!" } } BaseObject { Type = "BASE" Description = "da base" } ` dict := new(Dict) _, err := Decode(ex1, dict) if err != nil { t.Errorf("Decode error: %v", err) } if !reflect.DeepEqual(expected, dict) { t.Fatalf("\n%#v\n!=\n%#v\n", expected, dict) } } type sphere struct { Center [3]float64 Radius float64 } func TestDecodeSimpleArray(t *testing.T) { var s1 sphere if _, err := Decode(`center = [0.0, 1.5, 0.0]`, &s1); err != nil { t.Fatal(err) } } func TestDecodeArrayWrongSize(t *testing.T) { var s1 sphere if _, err := Decode(`center = [0.1, 2.3]`, &s1); err == nil { t.Fatal("Expected array type mismatch error") } } func TestDecodeLargeIntoSmallInt(t *testing.T) { type table struct { Value int8 } var tab table if _, err := Decode(`value = 500`, &tab); err == nil { t.Fatal("Expected integer out-of-bounds error.") } } func TestDecodeSizedInts(t *testing.T) { type table struct { U8 uint8 U16 uint16 U32 uint32 U64 uint64 U uint I8 int8 I16 int16 I32 int32 I64 int64 I int } answer := table{1, 1, 1, 1, 1, -1, -1, -1, -1, -1} configStr := ` u8 = 1 u16 = 1 u32 = 1 u64 = 1 u = 1 i8 = -1 i16 = -1 i32 = -1 i64 = -1 i = -1 ` var tab table if _, err := Decode(configStr, &tab); err != nil { t.Fatal(err.Error()) } if answer != tab { t.Fatalf("Expected %#v but got %#v", answer, tab) } } func ExampleMetaData_PrimitiveDecode() { var md MetaData var err error var rawData = ` ranking = ["Springsteen", "JGeils"] bands { Springsteen { started = 1973 albums = ["Greetings", "WIESS", "Born to Run", "Darkness"] } JGeils { started = 1970 albums = ["The J. Geils Band", "Full House", "Blow Your Face Out"] } } ` type band struct { Started int Albums []string } type classics struct { Ranking []string Bands map[string]Primitive } // Do the initial decode. Reflection is delayed on Primitive values. var music classics if md, err = Decode(rawData, &music); err != nil { log.Fatal(err) } // MetaData still includes information on Primitive values. fmt.Printf("Is `bands.Springsteen` defined? %v\n", md.IsDefined("bands", "Springsteen")) // Decode primitive data into Go values. for _, artist := range music.Ranking { // A band is a primitive value, so we need to decode it to get a // real `band` value. primValue := music.Bands[artist] var aBand band if err = md.PrimitiveDecode(primValue, &aBand); err != nil { u.Warnf("failed on: ") log.Fatalf("Failed on %v %v", primValue, err) } fmt.Printf("%s started in %d.\n", artist, aBand.Started) } // Check to see if there were any fields left undecoded. // Note that this won't be empty before decoding the Primitive value! fmt.Printf("Undecoded: %q\n", md.Undecoded()) // Output: // Is `bands.Springsteen` defined? true // Springsteen started in 1973. // JGeils started in 1970. // Undecoded: [] } func ExampleDecode() { var rawData = ` # Some comments. alpha { ip = "10.0.0.1" // config section config { Ports = [ 8001, 8002 ] Location = "Toronto" Created = 1987-07-05T05:45:00Z } } beta { ip = "10.0.0.2" config { Ports = [ 9001, 9002 ] Location = "New Jersey" Created = 1887-01-05T05:55:00Z } } ` type serverConfig struct { Ports []int Location string Created time.Time } type server struct { IP string `confl:"ip"` Config serverConfig `confl:"config"` } type servers map[string]server var config servers if _, err := Decode(rawData, &config); err != nil { log.Fatal(err) } for _, name := range []string{"alpha", "beta"} { s := config[name] fmt.Printf("Server: %s (ip: %s) in %s created on %s\n", name, s.IP, s.Config.Location, s.Config.Created.Format("2006-01-02")) fmt.Printf("Ports: %v\n", s.Config.Ports) } // Output: // Server: alpha (ip: 10.0.0.1) in Toronto created on 1987-07-05 // Ports: [8001 8002] // Server: beta (ip: 10.0.0.2) in New Jersey created on 1887-01-05 // Ports: [9001 9002] } type duration struct { time.Duration } func (d *duration) UnmarshalText(text []byte) error { var err error d.Duration, err = time.ParseDuration(string(text)) return err } // Example Unmarshaler shows how to decode strings into your own // custom data type. func Example_unmarshaler() { rawData := ` song [ { name = "Thunder Road" duration = "4m49s" }, { name = "Stairway to Heaven" duration = "8m03s" } ] ` type song struct { Name string Duration duration } type songs struct { Song []song } var favorites songs if _, err := Decode(rawData, &favorites); err != nil { log.Fatal(err) } // Code to implement the TextUnmarshaler interface for `duration`: // // type duration struct { // time.Duration // } // // func (d *duration) UnmarshalText(text []byte) error { // var err error // d.Duration, err = time.ParseDuration(string(text)) // return err // } for _, s := range favorites.Song { fmt.Printf("%s (%s)\n", s.Name, s.Duration) } // Output: // Thunder Road (4m49s) // Stairway to Heaven (8m3s) } // Example StrictDecoding shows how to detect whether there are keys in the // config document that weren't decoded into the value given. This is useful // for returning an error to the user if they've included extraneous fields // in their configuration. // func Example_strictDecoding() { // var rawData = ` // key1 = "value1" // key2 = "value2" // key3 = "value3" // ` // type config struct { // Key1 string // Key3 string // } // var conf config // md, err := Decode(rawData, &conf) // if err != nil { // log.Fatal(err) // } // fmt.Printf("Undecoded keys: %q\n", md.Undecoded()) // // Output: // // Undecoded keys: ["key2"] // } ================================================ FILE: doc.go ================================================ /* Package confl provides facilities for decoding and encoding TOML/NGINX configuration files via reflection. */ package confl ================================================ FILE: encode.go ================================================ package confl import ( "bufio" "bytes" "errors" "fmt" u "github.com/araddon/gou" "io" "reflect" "sort" "strconv" "strings" "time" ) type encodeError struct{ error } var ( errArrayMixedElementTypes = errors.New("can't encode array with mixed element types") errArrayNilElement = errors.New("can't encode array with nil element") errNonString = errors.New("can't encode a map with non-string key type") errAnonNonStruct = errors.New("can't encode an anonymous field that is not a struct") errArrayNoTable = errors.New("array element can't contain a table") errNoKey = errors.New("top-level values must be a Go map or struct") errAnything = errors.New("") // used in testing _ = u.EMPTY ) var quotedReplacer = strings.NewReplacer( "\t", "\\t", "\n", "\\n", "\r", "\\r", "\"", "\\\"", "\\", "\\\\", ) // Marshall a go struct into bytes func Marshal(v interface{}) ([]byte, error) { buf := bytes.Buffer{} enc := NewEncoder(&buf) err := enc.Encode(v) if err != nil { return nil, err } return buf.Bytes(), nil } // Encoder controls the encoding of Go values to a document to some // io.Writer. // // The indentation level can be controlled with the Indent field. type Encoder struct { // A single indentation level. By default it is two spaces. Indent string // hasWritten is whether we have written any output to w yet. hasWritten bool w *bufio.Writer } // NewEncoder returns a encoder that encodes Go values to the io.Writer // given. By default, a single indentation level is 2 spaces. func NewEncoder(w io.Writer) *Encoder { return &Encoder{ w: bufio.NewWriter(w), Indent: " ", } } // Encode writes a representation of the Go value to the underlying // io.Writer. If the value given cannot be encoded to a valid document, // then an error is returned. // // The mapping between Go values and values should be precisely the same // as for the Decode* functions. Similarly, the TextMarshaler interface is // supported by encoding the resulting bytes as strings. (If you want to write // arbitrary binary data then you will need to use something like base64 since // does not have any binary types.) // // When encoding hashes (i.e., Go maps or structs), keys without any // sub-hashes are encoded first. // // If a Go map is encoded, then its keys are sorted alphabetically for // deterministic output. More control over this behavior may be provided if // there is demand for it. // // Encoding Go values without a corresponding representation---like map // types with non-string keys---will cause an error to be returned. Similarly // for mixed arrays/slices, arrays/slices with nil elements, embedded // non-struct types and nested slices containing maps or structs. // (e.g., [][]map[string]string is not allowed but []map[string]string is OK // and so is []map[string][]string.) func (enc *Encoder) Encode(v interface{}) error { rv := eindirect(reflect.ValueOf(v)) if err := enc.safeEncode(Key([]string{}), rv); err != nil { return err } return enc.w.Flush() } func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) { defer func() { if r := recover(); r != nil { if terr, ok := r.(encodeError); ok { err = terr.error return } panic(r) } }() enc.encode(key, rv) return nil } func (enc *Encoder) encode(key Key, rv reflect.Value) { // Special case. Time needs to be in ISO8601 format. // Special case. If we can marshal the type to text, then we used that. // Basically, this prevents the encoder for handling these types as // generic structs (or whatever the underlying type of a TextMarshaler is). switch rv.Interface().(type) { case time.Time, TextMarshaler: enc.keyEqElement(key, rv) return } k := rv.Kind() //u.Debugf("key:%v len:%v val=%v", key.String(), len(key), k.String()) switch k { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: enc.keyEqElement(key, rv) case reflect.Array, reflect.Slice: if typeEqual(confArrayHash, confTypeOfGo(rv)) { enc.eArrayOfTables(key, rv) } else { enc.keyEqElement(key, rv) } case reflect.Interface: if rv.IsNil() { return } enc.encode(key, rv.Elem()) case reflect.Map: if rv.IsNil() { return } enc.eTable(key, rv) case reflect.Ptr: if rv.IsNil() { return } enc.encode(key, rv.Elem()) case reflect.Struct: enc.eTable(key, rv) default: panic(e("Unsupported type for key '%s': %s", key, k)) } } // eElement encodes any value that can be an array element (primitives and // arrays). func (enc *Encoder) eElement(rv reflect.Value) { switch v := rv.Interface().(type) { case time.Time: // Special case time.Time as a primitive. Has to come before // TextMarshaler below because time.Time implements // encoding.TextMarshaler, but we need to always use UTC. enc.wf(v.In(time.FixedZone("UTC", 0)).Format("2006-01-02T15:04:05Z")) return case TextMarshaler: // Special case. Use text marshaler if it's available for this value. if s, err := v.MarshalText(); err != nil { encPanic(err) } else { enc.writeQuoted(string(s)) } return } switch rv.Kind() { case reflect.Bool: enc.wf(strconv.FormatBool(rv.Bool())) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: enc.wf(strconv.FormatInt(rv.Int(), 10)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: enc.wf(strconv.FormatUint(rv.Uint(), 10)) case reflect.Float32: enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32))) case reflect.Float64: enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64))) case reflect.Array, reflect.Slice: enc.eArrayOrSliceElement(rv) case reflect.Interface: enc.eElement(rv.Elem()) case reflect.String: enc.writeQuoted(rv.String()) default: panic(e("Unexpected primitive type: %s", rv.Kind())) } } // all floats must have a decimal with at least one number on either side. func floatAddDecimal(fstr string) string { if !strings.Contains(fstr, ".") { return fstr + ".0" } return fstr } func (enc *Encoder) writeQuoted(s string) { enc.wf("\"%s\"", quotedReplacer.Replace(s)) } func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) { length := rv.Len() //u.Infof("arrayorslice? %v", rv) enc.wf("[") for i := 0; i < length; i++ { elem := rv.Index(i) enc.eElement(elem) if i != length-1 { enc.wf(", ") } } enc.wf("]") } func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) { if len(key) == 0 { encPanic(errNoKey) } //u.Debugf("eArrayOfTables key=%s key.len=%v rv=%v", key, len(key), rv) panicIfInvalidKey(key, true) //enc.newline() //enc.wf("%s [\n%s]]", enc.indentStr(key), key.String()) newKey := key.insert("_") keyDelta := 0 enc.wf("%s%s = [", enc.indentStrDelta(key, -1), key[len(key)-1]) for i := 0; i < rv.Len(); i++ { trv := rv.Index(i) if isNil(trv) { continue } enc.newline() enc.wf("%s{", enc.indentStrDelta(key, keyDelta)) enc.newline() //enc.wf("%s{\n%s", enc.indentStr(key), key.String()) //enc.newline() enc.eMapOrStruct(newKey, trv) //enc.newline() if i == rv.Len()-1 { enc.wf("%s}", enc.indentStrDelta(key, keyDelta)) } else { enc.wf("%s},", enc.indentStrDelta(key, keyDelta)) } } enc.newline() enc.wf("%s]", enc.indentStrDelta(key, -1)) enc.newline() } func (enc *Encoder) eTable(key Key, rv reflect.Value) { if len(key) == 1 { // Output an extra new line between top-level tables. // (The newline isn't written if nothing else has been written though.) //enc.newline() } if len(key) > 0 { panicIfInvalidKey(key, true) //u.Infof("table? %v %v", key, rv) enc.wf("%s%s {", enc.indentStrDelta(key, -1), key[len(key)-1]) enc.newline() } enc.eMapOrStruct(key, rv) if len(key) > 0 { enc.wf("%s}", enc.indentStrDelta(key, -1)) enc.newline() } } func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) { switch rv := eindirect(rv); rv.Kind() { case reflect.Map: enc.eMap(key, rv) case reflect.Struct: enc.eStruct(key, rv) default: panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String()) } } func (enc *Encoder) eMap(key Key, rv reflect.Value) { rt := rv.Type() if rt.Key().Kind() != reflect.String { encPanic(errNonString) } // Sort keys so that we have deterministic output. And write keys directly // underneath this key first, before writing sub-structs or sub-maps. var mapKeysDirect, mapKeysSub []string for _, mapKey := range rv.MapKeys() { k := mapKey.String() //u.Infof("map key: %v", k) if typeIsHash(confTypeOfGo(rv.MapIndex(mapKey))) { //u.Debugf("found sub? %s for %v", k, confTypeOfGo(rv.MapIndex(mapKey))) mapKeysSub = append(mapKeysSub, k) } else { mapKeysDirect = append(mapKeysDirect, k) } } var writeMapKeys = func(mapKeys []string) { sort.Strings(mapKeys) for _, mapKey := range mapKeys { //u.Infof("mapkey: %v", mapKey) mrv := rv.MapIndex(reflect.ValueOf(mapKey)) if isNil(mrv) { // Don't write anything for nil fields. continue } enc.encode(key.add(mapKey), mrv) } } writeMapKeys(mapKeysDirect) writeMapKeys(mapKeysSub) } func (enc *Encoder) eStruct(key Key, rv reflect.Value) { // Write keys for fields directly under this key first, because if we write // a field that creates a new table, then all keys under it will be in that // table (not the one we're writing here). rt := rv.Type() var fieldsDirect, fieldsSub [][]int var addFields func(rt reflect.Type, rv reflect.Value, start []int) addFields = func(rt reflect.Type, rv reflect.Value, start []int) { for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) // skip unexporded fields if f.PkgPath != "" { continue } frv := rv.Field(i) if f.Anonymous { frv := eindirect(frv) t := frv.Type() if t.Kind() != reflect.Struct { encPanic(errAnonNonStruct) } addFields(t, frv, f.Index) } else if typeIsHash(confTypeOfGo(frv)) { fieldsSub = append(fieldsSub, append(start, f.Index...)) } else { fieldsDirect = append(fieldsDirect, append(start, f.Index...)) } } } addFields(rt, rv, nil) var writeFields = func(fields [][]int) { for _, fieldIndex := range fields { sft := rt.FieldByIndex(fieldIndex) sf := rv.FieldByIndex(fieldIndex) if isNil(sf) { // Don't write anything for nil fields. continue } keyName := sft.Tag.Get("confl") if keyName == "-" { continue } if keyName == "" { keyName = sft.Tag.Get("json") if keyName == "-" { continue } else if keyName == "" { keyName = sft.Name } } //u.Infof("found key: depth?%v keyName='%v'\t\tsf=%v", len(key), keyName, sf) enc.encode(key.add(keyName), sf) } } writeFields(fieldsDirect) writeFields(fieldsSub) } // returns the Confl type name of the Go value's type. It is used to // determine whether the types of array elements are mixed (which is forbidden). // If the Go value is nil, then it is illegal for it to be an array element, and // valueIsNil is returned as true. // Returns the confl type of a Go value. The type may be `nil`, which means // no concrete confl type could be found. func confTypeOfGo(rv reflect.Value) confType { if isNil(rv) || !rv.IsValid() { return nil } switch rv.Kind() { case reflect.Bool: return confBool case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return confInteger case reflect.Float32, reflect.Float64: return confFloat case reflect.Array, reflect.Slice: if typeEqual(confHash, confArrayType(rv)) { return confArrayHash } else { return confArray } case reflect.Ptr, reflect.Interface: return confTypeOfGo(rv.Elem()) case reflect.String: return confString case reflect.Map: return confHash case reflect.Struct: switch rv.Interface().(type) { case time.Time: return confDatetime case TextMarshaler: return confString default: return confHash } default: panic("unexpected reflect.Kind: " + rv.Kind().String()) } } // returns the element type of a array. The type returned // may be nil if it cannot be determined (e.g., a nil slice or a zero length // slize). This function may also panic if it finds a type that cannot be // expressed in (such as nil elements, heterogeneous arrays or directly // nested arrays of tables). func confArrayType(rv reflect.Value) confType { if isNil(rv) || !rv.IsValid() || rv.Len() == 0 { return nil } firstType := confTypeOfGo(rv.Index(0)) if firstType == nil { encPanic(errArrayNilElement) } rvlen := rv.Len() for i := 1; i < rvlen; i++ { elem := rv.Index(i) switch elemType := confTypeOfGo(elem); { case elemType == nil: encPanic(errArrayNilElement) case !typeEqual(firstType, elemType): encPanic(errArrayMixedElementTypes) } } // If we have a nested array, then we must make sure that the nested // array contains ONLY primitives. // This checks arbitrarily nested arrays. if typeEqual(firstType, confArray) || typeEqual(firstType, confArrayHash) { nest := confArrayType(eindirect(rv.Index(0))) if typeEqual(nest, confHash) || typeEqual(nest, confArrayHash) { encPanic(errArrayNoTable) } } return firstType } func (enc *Encoder) newline() { if enc.hasWritten { enc.wf("\n") } } func (enc *Encoder) keyEqElement(key Key, val reflect.Value) { if len(key) == 0 { encPanic(errNoKey) } panicIfInvalidKey(key, false) //u.Infof("keyEqElement: %v", key[len(key)-1]) enc.wf("%s%s = ", enc.indentStrDelta(key, -1), key[len(key)-1]) enc.eElement(val) enc.newline() } func (enc *Encoder) wf(format string, v ...interface{}) { if _, err := fmt.Fprintf(enc.w, format, v...); err != nil { encPanic(err) } enc.hasWritten = true } func (enc *Encoder) indentStr(key Key) string { return strings.Repeat(enc.Indent, len(key)) } func (enc *Encoder) indentStrDelta(key Key, delta int) string { return strings.Repeat(enc.Indent, len(key)+delta) } func encPanic(err error) { panic(encodeError{err}) } func eindirect(v reflect.Value) reflect.Value { switch v.Kind() { case reflect.Ptr, reflect.Interface: return eindirect(v.Elem()) default: return v } } func isNil(rv reflect.Value) bool { switch rv.Kind() { case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: return rv.IsNil() default: return false } } func panicIfInvalidKey(key Key, hash bool) { if hash { for _, k := range key { if !isValidTableName(k) { encPanic(e("Key '%s' is not a valid table name. Table names "+ "cannot contain '[', ']' or '.'.", key.String())) } } } else { if !isValidKeyName(key[len(key)-1]) { encPanic(e("Key '%s' is not a name. Key names "+ "cannot contain whitespace.", key.String())) } } } func isValidTableName(s string) bool { if len(s) == 0 { return false } for _, r := range s { if r == '[' || r == ']' || r == '.' { return false } } return true } func isValidKeyName(s string) bool { if len(s) == 0 { return false } return true } ================================================ FILE: encode_test.go ================================================ package confl import ( "bytes" "fmt" "log" "net" "testing" "time" u "github.com/araddon/gou" "github.com/stretchr/testify/assert" ) var _ = u.EMPTY func TestEncodeRoundTrip(t *testing.T) { type Config struct { Age int Cats []string Pi float64 Perfection []int DOB time.Time Ipaddress net.IP } var inputs = Config{ 13, []string{"one", "two", "three"}, 3.145, []int{11, 2, 3, 4}, time.Now(), net.ParseIP("192.168.59.254"), } var firstBuffer bytes.Buffer e := NewEncoder(&firstBuffer) err := e.Encode(inputs) if err != nil { t.Fatal(err) } var outputs Config if _, err := Decode(firstBuffer.String(), &outputs); err != nil { log.Printf("Could not decode:\n-----\n%s\n-----\n", firstBuffer.String()) t.Fatal(err) } // could test each value individually, but I'm lazy var secondBuffer bytes.Buffer e2 := NewEncoder(&secondBuffer) err = e2.Encode(outputs) if err != nil { t.Fatal(err) } if firstBuffer.String() != secondBuffer.String() { t.Error( firstBuffer.String(), "\n\n is not identical to\n\n", secondBuffer.String()) } // now try with Marshal marshalBytes, err := Marshal(&inputs) assert.Equal(t, nil, err) assert.True(t, bytes.Equal(marshalBytes, firstBuffer.Bytes()), "equal?") } func TestEncodeMany(t *testing.T) { type Embedded struct { Int int `confl:"_int"` } type NonStruct int date := time.Date(2014, 5, 11, 20, 30, 40, 0, time.FixedZone("IST", 3600)) dateStr := "2014-05-11T19:30:40Z" tests := []struct { label string input interface{} wantOutput string wantError error }{ {label: "bool field", input: struct { BoolTrue bool BoolFalse bool }{true, false}, wantOutput: "BoolTrue = true\nBoolFalse = false\n", }, {label: "int fields", input: struct { Int int Int8 int8 Int16 int16 Int32 int32 Int64 int64 }{1, 2, 3, 4, 5}, wantOutput: "Int = 1\nInt8 = 2\nInt16 = 3\nInt32 = 4\nInt64 = 5\n", }, {label: "uint fields", input: struct { Uint uint Uint8 uint8 Uint16 uint16 Uint32 uint32 Uint64 uint64 }{1, 2, 3, 4, 5}, wantOutput: "Uint = 1\nUint8 = 2\nUint16 = 3\nUint32 = 4" + "\nUint64 = 5\n", }, {label: "float fields", input: struct { Float32 float32 Float64 float64 }{1.5, 2.5}, wantOutput: "Float32 = 1.5\nFloat64 = 2.5\n", }, {label: "string field", input: struct{ String string }{"foo"}, wantOutput: "String = \"foo\"\n", }, {label: "string field and unexported field", input: struct { String string unexported int }{"foo", 0}, wantOutput: "String = \"foo\"\n", }, {label: "datetime field in UTC", input: struct{ Date time.Time }{date}, wantOutput: fmt.Sprintf("Date = %s\n", dateStr), }, {label: "datetime field as primitive", // Using a map here to fail if isStructOrMap() returns true for // time.Time. input: map[string]interface{}{ "Date": date, "Int": 1, }, wantOutput: fmt.Sprintf("Date = %s\nInt = 1\n", dateStr), }, {label: "array fields", input: struct { IntArray0 [0]int IntArray3 [3]int }{[0]int{}, [3]int{1, 2, 3}}, wantOutput: "IntArray0 = []\nIntArray3 = [1, 2, 3]\n", }, {label: "slice fields", input: struct{ IntSliceNil, IntSlice0, IntSlice3 []int }{ nil, []int{}, []int{1, 2, 3}, }, wantOutput: "IntSlice0 = []\nIntSlice3 = [1, 2, 3]\n", }, {label: "datetime slices", input: struct{ DatetimeSlice []time.Time }{ []time.Time{date, date}, }, wantOutput: fmt.Sprintf("DatetimeSlice = [%s, %s]\n", dateStr, dateStr), }, {label: "nested arrays and slices", input: struct { SliceOfArrays [][2]int ArrayOfSlices [2][]int SliceOfArraysOfSlices [][2][]int ArrayOfSlicesOfArrays [2][][2]int SliceOfMixedArrays [][2]interface{} ArrayOfMixedSlices [2][]interface{} }{ [][2]int{{1, 2}, {3, 4}}, [2][]int{{1, 2}, {3, 4}}, [][2][]int{ { {1, 2}, {3, 4}, }, { {5, 6}, {7, 8}, }, }, [2][][2]int{ { {1, 2}, {3, 4}, }, { {5, 6}, {7, 8}, }, }, [][2]interface{}{ {1, 2}, {"a", "b"}, }, [2][]interface{}{ {1, 2}, {"a", "b"}, }, }, wantOutput: `SliceOfArrays = [[1, 2], [3, 4]] ArrayOfSlices = [[1, 2], [3, 4]] SliceOfArraysOfSlices = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] ArrayOfSlicesOfArrays = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] SliceOfMixedArrays = [[1, 2], ["a", "b"]] ArrayOfMixedSlices = [[1, 2], ["a", "b"]] `, }, {label: "empty slice", input: struct{ Empty []interface{} }{[]interface{}{}}, wantOutput: "Empty = []\n", }, {label: "(error) slice with element type mismatch (string and integer)", input: struct{ Mixed []interface{} }{[]interface{}{1, "a"}}, wantError: errArrayMixedElementTypes, }, {label: "(error) slice with element type mismatch (integer and float)", input: struct{ Mixed []interface{} }{[]interface{}{1, 2.5}}, wantError: errArrayMixedElementTypes, }, {label: "slice with elems of differing Go types, same types", input: struct { MixedInts []interface{} MixedFloats []interface{} }{ []interface{}{ int(1), int8(2), int16(3), int32(4), int64(5), uint(1), uint8(2), uint16(3), uint32(4), uint64(5), }, []interface{}{float32(1.5), float64(2.5)}, }, wantOutput: "MixedInts = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]\n" + "MixedFloats = [1.5, 2.5]\n", }, {label: "(error) slice w/ element type mismatch (one is nested array)", input: struct{ Mixed []interface{} }{ []interface{}{1, []interface{}{2}}, }, wantError: errArrayMixedElementTypes, }, {label: "(error) slice with 1 nil element", input: struct{ NilElement1 []interface{} }{[]interface{}{nil}}, wantError: errArrayNilElement, }, {label: "(error) slice with 1 nil element (and other non-nil elements)", input: struct{ NilElement []interface{} }{ []interface{}{1, nil}, }, wantError: errArrayNilElement, }, {label: "simple map", input: map[string]int{"a": 1, "b": 2}, wantOutput: "a = 1\nb = 2\n", }, {label: "map with interface{} value type", input: map[string]interface{}{"a": 1, "b": "c"}, wantOutput: "a = 1\nb = \"c\"\n", }, {label: "map with interface{} value type, some of which are structs", input: map[string]interface{}{ "a": struct{ Int int }{2}, "b": 1, }, wantOutput: "b = 1\na {\n Int = 2\n}\n", }, {label: "nested map", input: map[string]map[string]int{ "a": {"b": 1}, "c": {"d": 2}, }, wantOutput: "a {\n b = 1\n}\nc {\n d = 2\n}\n", }, {label: "nested struct", input: struct{ Struct struct{ Int int } }{ struct{ Int int }{1}, }, wantOutput: "Struct {\n Int = 1\n}\n", }, {label: "nested struct and non-struct field", input: struct { Struct struct{ Int int } Bool bool }{struct{ Int int }{1}, true}, wantOutput: "Bool = true\nStruct {\n Int = 1\n}\n", }, {label: "2 nested structs", input: struct{ Struct1, Struct2 struct{ Int int } }{ struct{ Int int }{1}, struct{ Int int }{2}, }, wantOutput: "Struct1 {\n Int = 1\n}\nStruct2 {\n Int = 2\n}\n", }, {label: "deeply nested structs", input: struct { Struct1, Struct2 struct{ Struct3 *struct{ Int int } } }{ struct{ Struct3 *struct{ Int int } }{&struct{ Int int }{1}}, struct{ Struct3 *struct{ Int int } }{nil}, }, wantOutput: "Struct1 {\n Struct3 {\n Int = 1\n }\n}\nStruct2 {\n}\n", }, {label: "nested struct with nil struct elem", input: struct { Struct struct{ Inner *struct{ Int int } } }{ struct{ Inner *struct{ Int int } }{nil}, }, wantOutput: "Struct {\n}\n", }, {label: "nested struct with no fields", input: struct { Struct struct{ Inner struct{} } }{ struct{ Inner struct{} }{struct{}{}}, }, wantOutput: "Struct {\n Inner {\n }\n}\n", }, {label: "struct with tags", input: struct { Struct struct { Int int `json:"_int"` } `confl:"_struct"` Bool bool `confl:"_bool"` }{ struct { Int int `json:"_int"` }{1}, true, }, wantOutput: "_bool = true\n_struct {\n _int = 1\n}\n", }, {label: "embedded struct", input: struct{ Embedded }{Embedded{1}}, wantOutput: "_int = 1\n", }, {label: "embedded *struct", input: struct{ *Embedded }{&Embedded{1}}, wantOutput: "_int = 1\n", }, {label: "nested embedded struct", input: struct { Struct struct{ Embedded } `confl:"_struct"` }{struct{ Embedded }{Embedded{1}}}, wantOutput: "_struct {\n _int = 1\n}\n", }, {label: "nested embedded *struct", input: struct { Struct struct{ *Embedded } `confl:"_struct"` }{struct{ *Embedded }{&Embedded{1}}}, wantOutput: "_struct {\n _int = 1\n}\n", }, {label: "array of tables", input: struct { Structs []*struct{ Int int } `confl:"struct"` }{ []*struct{ Int int }{{1}, {3}}, }, wantOutput: "struct = [\n {\n Int = 1\n },\n {\n Int = 3\n }\n]\n", }, {label: "array of tables order", input: map[string]interface{}{ "map": map[string]interface{}{ "zero": 5, "arr": []map[string]int{ map[string]int{ "friend": 5, }, }, }, }, wantOutput: "map {\n zero = 5\n arr = [\n {\n friend = 5\n }\n ]\n}\n", }, {label: "(error) top-level slice", input: []struct{ Int int }{{1}, {2}, {3}}, wantError: errNoKey, }, {label: "(error) slice of slice", input: struct { Slices [][]struct{ Int int } }{ [][]struct{ Int int }{{{1}}, {{2}}, {{3}}}, }, wantError: errArrayNoTable, }, {label: "(error) map no string key", input: map[int]string{1: ""}, wantError: errNonString, }, {label: "(error) anonymous non-struct", input: struct{ NonStruct }{5}, wantError: errAnonNonStruct, }, {label: "(error) empty key name", input: map[string]int{"": 1}, wantError: errAnything, }, {label: "(error) empty map name", input: map[string]interface{}{ "": map[string]int{"v": 1}, }, wantError: errAnything, }, } for idx, test := range tests { u.Debugf("starting test: #%d %v", idx, test.label) encodeExpected(t, fmt.Sprintf("#%d: %s", idx, test.label), test.input, test.wantOutput, test.wantError) } } func TestEncodeNestedTableArrays(t *testing.T) { type song struct { Name string `confl:"name"` } type album struct { Name string `confl:"name"` Songs []song `confl:"songs"` } type springsteen struct { Albums []album `confl:"albums"` } value := springsteen{ []album{ {"Born to Run", []song{{"Jungleland"}, {"Meeting Across the River"}}}, {"Born in the USA", []song{{"Glory Days"}, {"Dancing in the Dark"}}}, }, } expected := `albums = [ { name = "Born to Run" songs = [ { name = "Jungleland" }, { name = "Meeting Across the River" } ] }, { name = "Born in the USA" songs = [ { name = "Glory Days" }, { name = "Dancing in the Dark" } ] } ] ` encodeExpected(t, "nested table arrays", value, expected, nil) } func TestEncodeArrayHashWithNormalHashOrder(t *testing.T) { type Alpha struct { V int } type Beta struct { V int } type Conf struct { V int A Alpha B []Beta } val := Conf{ V: 1, A: Alpha{2}, B: []Beta{{3}}, } expected := "V = 1\nA {\n V = 2\n}\nB = [\n {\n V = 3\n }\n]\n" encodeExpected(t, "array hash with normal hash order", val, expected, nil) } func encodeExpected( t *testing.T, label string, val interface{}, wantStr string, wantErr error, ) { var buf bytes.Buffer enc := NewEncoder(&buf) err := enc.Encode(val) if err != wantErr { if wantErr != nil { if wantErr == errAnything && err != nil { return } t.Errorf("%s: want Encode error %v, got %v", label, wantErr, err) } else { t.Errorf("%s: Encode failed: %s", label, err) } } if err != nil { return } if got := buf.String(); wantStr != got { u.Debugf("\n\n%s wanted: \n%s\ngot: \n%s", label, wantStr, got) for pos, r := range wantStr { if len(got)-1 <= pos { u.Warnf("len mismatch? %v vs %v", len(got), len(wantStr)) } else if r != rune(got[pos]) { u.Warnf("mismatch at position: %v %s!=%s", pos, string(r), string(got[pos])) break } } t.Fatalf("%s: want\n-----\n%q\n-----\nbut got\n-----\n%q\n-----\n", label, wantStr, got) } } func ExampleEncoder_Encode() { date, _ := time.Parse(time.RFC822, "14 Mar 10 18:00 UTC") var config = map[string]interface{}{ "date": date, "counts": []int{1, 1, 2, 3, 5, 8}, "hash": map[string]string{ "key1": "val1", "key2": "val2", }, } buf := new(bytes.Buffer) if err := NewEncoder(buf).Encode(config); err != nil { u.Errorf("could not encode: %v", err) log.Fatal(err) } fmt.Println(buf.String()) // Output: // counts = [1, 1, 2, 3, 5, 8] // date = 2010-03-14T18:00:00Z // hash { // key1 = "val1" // key2 = "val2" // } } ================================================ FILE: encoding_types.go ================================================ // +build go1.2 package confl // In order to support Go 1.1, we define our own TextMarshaler and // TextUnmarshaler types. For Go 1.2+, we just alias them with the // standard library interfaces. import ( "encoding" ) // TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here // so that Go 1.1 can be supported. type TextMarshaler encoding.TextMarshaler // TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here // so that Go 1.1 can be supported. type TextUnmarshaler encoding.TextUnmarshaler ================================================ FILE: encoding_types_1.1.go ================================================ // +build !go1.2 package confl // These interfaces were introduced in Go 1.2, so we add them manually when // compiling for Go 1.1. // TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here // so that Go 1.1 can be supported. type TextMarshaler interface { MarshalText() (text []byte, err error) } // TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here // so that Go 1.1 can be supported. type TextUnmarshaler interface { UnmarshalText(text []byte) error } ================================================ FILE: fuzz.go ================================================ // +build gofuzz package confl /* Fuzz testing support files https://github.com/dvyukov/go-fuzz Usage: go-fuzz-build github.com/lytics/confl mkdir fuzz cp _examples/*.conf fuzz/ go-fuzz -bin=confl-fuzz.zip -workdir=fuzz See fuzz/crashers for results. */ func Fuzz(data []byte) int { var v map[string]interface{} if err := Unmarshal(data, &v); err != nil { return 0 } return 1 } ================================================ FILE: go.test.sh ================================================ #!/usr/bin/env bash set -e echo "" > coverage.txt for d in $(go list ./... | grep -v vendor); do go test -race -coverprofile=profile.out -covermode=atomic $d if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi done ================================================ FILE: lex.go ================================================ // Copyright 2013 Apcera Inc. All rights reserved. // Customized heavily from // https://github.com/BurntSushi/toml/blob/master/lex.go, which is based on // Rob Pike's talk: http://cuddle.googlecode.com/hg/talk/lex.html // The format supported is less restrictive than today's formats. // Supports mixed Arrays [], nested Maps {}, multiple comment types (# and //) // Also supports key value assigments using '=' or ':' or whiteSpace() // e.g. foo = 2, foo : 2, foo 2 // maps can be assigned with no key separator as well // semicolons as value terminators in key/value assignments are optional // // see lex_test.go for more examples. package confl import ( "fmt" "strings" "unicode" "unicode/utf8" ) var ( // IdentityChars Which Identity Characters are allowed? IdentityChars = "_." ) type itemType int const ( itemError itemType = iota itemNIL // used in the parser to indicate no type itemEOF itemKey itemText itemString itemBool itemInteger itemFloat itemDatetime itemArrayStart itemArrayEnd itemMapStart itemMapEnd itemCommentStart ) const ( eof = 0 mapStart = '{' mapEnd = '}' keySepEqual = '=' keySepColon = ':' arrayStart = '[' arrayEnd = ']' arrayValTerm = ',' mapValTerm = ',' commentHashStart = '#' commentSlashStart = '/' dqStringStart = '"' dqStringEnd = '"' sqStringStart = '\'' sqStringEnd = '\'' optValTerm = ';' blockStart = '(' blockEnd = ')' ) type stateFn func(lx *lexer) stateFn type lexer struct { input string start int pos int width int line int state stateFn items chan item circuitBreaker int isEnd func(lx *lexer, r rune) bool // A stack of state functions used to maintain context. // The idea is to reuse parts of the state machine in various places. // For example, values can appear at the top level or within arbitrarily // nested arrays. The last state on the stack is used after a value has // been lexed. Similarly for comments. stack []stateFn } type item struct { typ itemType val string line int } func (lx *lexer) nextItem() item { for { select { case item := <-lx.items: return item default: lx.state = lx.state(lx) } } } func lex(input string) *lexer { lx := &lexer{ input: input, state: lexTop, line: 1, items: make(chan item, 10), stack: make([]stateFn, 0, 10), isEnd: isEndNormal, } return lx } func (lx *lexer) push(state stateFn) { lx.stack = append(lx.stack, state) } func (lx *lexer) pop() stateFn { if len(lx.stack) == 0 { return lx.errorf("BUG in lexer: no states to pop.") } li := len(lx.stack) - 1 last := lx.stack[li] lx.stack = lx.stack[0:li] return last } func (lx *lexer) emit(typ itemType) { lx.items <- item{typ, lx.input[lx.start:lx.pos], lx.line} lx.start = lx.pos } func (lx *lexer) next() (r rune) { // stackBuf := make([]byte, 4096) // stackBufLen := runtime.Stack(stackBuf, false) // stackTraceStr := string(stackBuf[0:stackBufLen]) if lx.pos >= len(lx.input) { //u.Warnf("next() pos=%d len=%d %v", lx.pos, len(lx.input), string(stackTraceStr)) lx.width = 0 // if lx.circuitBreaker > 0 { // panic("hm") // } // lx.circuitBreaker++ return eof } //u.Debugf("next() pos=%d len=%d %v", lx.pos, len(lx.input), string(stackTraceStr)) if lx.input[lx.pos] == '\n' { lx.line++ } r, lx.width = utf8.DecodeRuneInString(lx.input[lx.pos:]) lx.pos += lx.width return r } // ignore skips over the pending input before this point. func (lx *lexer) ignore() { lx.start = lx.pos } // backup steps back one rune. Can be called only once per call of next. func (lx *lexer) backup() { // This backup has a problem, if eof has already been hit // lx.width will be = 0 // possibly just manually set to 1? lx.pos -= lx.width if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' { lx.line-- } } // peek returns but does not consume the next rune in the input. func (lx *lexer) peek() rune { r := lx.next() lx.backup() return r } // errorf stops all lexing by emitting an error and returning `nil`. // Note that any value that is a character is escaped if it's a special // character (new lines, tabs, etc.). func (lx *lexer) errorf(format string, values ...interface{}) stateFn { for i, value := range values { if v, ok := value.(rune); ok { values[i] = escapeSpecial(v) } } lx.items <- item{ itemError, fmt.Sprintf(format, values...), lx.line, } return nil } // lexTop consumes elements at the top level of data. func lexTop(lx *lexer) stateFn { r := lx.next() if r != eof && (isWhitespace(r) || isNL(r)) { return lexSkip(lx, lexTop) } switch { case r == commentHashStart: lx.push(lexTop) return lexCommentStart case r == commentSlashStart: rn := lx.next() if rn == commentSlashStart { lx.push(lexTop) return lexCommentStart } lx.backup() fallthrough case r == eof: if lx.pos > lx.start { return lx.errorf("Unexpected EOF.") } lx.emit(itemEOF) return nil } // At this point, the only valid item can be a key, so we back up // and let the key lexer do the rest. lx.backup() lx.push(lexTopValueEnd) return lexKeyStart } // lexTopValueEnd is entered whenever a top-level value has been consumed. // It must see only whitespace, and will turn back to lexTop upon a new line. // If it sees EOF, it will quit the lexer successfully. func lexTopValueEnd(lx *lexer) stateFn { r := lx.next() switch { case r == commentHashStart: // a comment will read to a new line for us. lx.push(lexTop) return lexCommentStart case r == commentSlashStart: rn := lx.next() if rn == commentSlashStart { lx.push(lexTop) return lexCommentStart } lx.backup() fallthrough case isWhitespace(r): return lexTopValueEnd case isNL(r) || r == eof || r == optValTerm: lx.ignore() return lexTop } return lx.errorf("Expected a top-level value to end with a new line, "+ "comment or EOF, but got '%v' instead.", r) } // lexKeyStart consumes a key name up until the first non-whitespace character. // lexKeyStart will ignore whitespace. It will also eat enclosing quotes. func lexKeyStart(lx *lexer) stateFn { r := lx.peek() switch { case isKeySeparator(r): return lx.errorf("Unexpected key separator '%v'.", r) case isWhitespace(r) || isNL(r): // Most likely this is not-reachable, as the lexTop already checks for it. lx.next() return lexSkip(lx, lexKeyStart) case r == dqStringStart: lx.next() return lexSkip(lx, lexDubQuotedKey) case r == sqStringStart: lx.next() return lexSkip(lx, lexQuotedKey) case !isIdentifierRune(r): // This is not a valid identity/key rune lx.next() lx.ignore() return lexKeyStart case r == eof: return lexTop } lx.ignore() lx.next() return lexKey } // lexDubQuotedKey consumes the text of a key between quotes. func lexDubQuotedKey(lx *lexer) stateFn { r := lx.peek() if r == dqStringEnd { lx.emit(itemKey) lx.next() return lexSkip(lx, lexKeyEnd) } lx.next() return lexDubQuotedKey } // lexQuotedKey consumes the text of a key between quotes. func lexQuotedKey(lx *lexer) stateFn { r := lx.peek() if r == sqStringEnd { lx.emit(itemKey) lx.next() return lexSkip(lx, lexKeyEnd) } lx.next() return lexQuotedKey } // lexKey consumes the text of a key. Assumes that the first character (which // is not whitespace) has already been consumed. func lexKey(lx *lexer) stateFn { r := lx.peek() switch { case r == eof: // Unexpected end, allow lexTop eof/error to handle it return lexTop case isWhitespace(r) || isNL(r) || isKeySeparator(r): lx.emit(itemKey) return lexKeyEnd } lx.next() return lexKey } // lexKeyEnd consumes the end of a key (up to the key separator). // Assumes that the first whitespace character after a key (or the '=' or ':' // separator) has NOT been consumed. func lexKeyEnd(lx *lexer) stateFn { r := lx.next() switch { case isWhitespace(r) || isNL(r): return lexSkip(lx, lexKeyEnd) case isKeySeparator(r): return lexSkip(lx, lexValue) } // We start the value here lx.backup() return lexValue } // lexValue starts the consumption of a value anywhere a value is expected. // lexValue will ignore whitespace. // After a value is lexed, the last state on the next is popped and returned. func lexValue(lx *lexer) stateFn { // We allow whitespace to precede a value, but NOT new lines. // In array syntax, the array states are responsible for ignoring new lines. r := lx.next() //u.Infof("lexValue r = %v", string(r)) if isWhitespace(r) { return lexSkip(lx, lexValue) } switch { case r == arrayStart: lx.ignore() lx.emit(itemArrayStart) lx.isEnd = isEndArrayUnQuoted return lexArrayValue case r == mapStart: lx.ignore() lx.emit(itemMapStart) return lexMapKeyStart case r == sqStringStart: // single quote: ' lx.ignore() // ignore the " or ' return lexQuotedString case r == dqStringStart: // " lx.ignore() // ignore the " or ' return lexDubQuotedString case r == '-': return lexNumberStart case r == blockStart: lx.next() // ignore the /n after { lx.ignore() // Ignore the ( return lexBlock case isDigit(r): lx.backup() // avoid an extra state and use the same as above return lexNumberOrDateStart case r == '.': // special error case, be kind to users return lx.errorf("Floats must start with a digit, not '.'.") case isNL(r): return lx.errorf("Expected value but found new line") } // we didn't consume it, so backup lx.backup() return lexString //return lx.errorf("Expected value but found '%s' instead.", r) } // lexArrayValue consumes one value in an array. It assumes that '[' or ',' // have already been consumed. All whitespace and new lines are ignored. func lexArrayValue(lx *lexer) stateFn { r := lx.next() //u.Infof("lexArrayValue r = %v", string(r)) switch { case isWhitespace(r) || isNL(r): return lexSkip(lx, lexArrayValue) case r == commentHashStart: lx.push(lexArrayValue) return lexCommentStart case r == commentSlashStart: rn := lx.next() if rn == commentSlashStart { lx.push(lexArrayValue) return lexCommentStart } lx.backup() fallthrough case r == arrayValTerm: // , we should not have found comma yet return lx.errorf("Unexpected array value terminator '%v'.", arrayValTerm) case r == arrayEnd: return lexArrayEnd } lx.backup() lx.push(lexArrayValueEnd) return lexValue } // lexArrayValueEnd consumes the cruft between values of an array. Namely, // it ignores whitespace and expects either a ',' or a ']'. func lexArrayValueEnd(lx *lexer) stateFn { r := lx.next() //u.Infof("lexArrayValueEnd r = %v", string(r)) switch { case isWhitespace(r): return lexSkip(lx, lexArrayValueEnd) case r == commentHashStart: lx.push(lexArrayValueEnd) return lexCommentStart case r == commentSlashStart: rn := lx.next() if rn == commentSlashStart { lx.push(lexArrayValueEnd) return lexCommentStart } lx.backup() fallthrough case r == arrayValTerm || isNL(r): return lexSkip(lx, lexArrayValue) // Move onto next case r == arrayEnd: return lexArrayEnd } return lx.errorf("Expected an array value terminator %q or an array "+ "terminator %q, but got '%v' instead.", arrayValTerm, arrayEnd, r) } // lexArrayEnd finishes the lexing of an array. It assumes that a ']' has // just been consumed. func lexArrayEnd(lx *lexer) stateFn { lx.ignore() lx.emit(itemArrayEnd) lx.isEnd = isEndNormal return lx.pop() } // lexMapKeyStart consumes a key name up until the first non-whitespace // character. // lexMapKeyStart will ignore whitespace. func lexMapKeyStart(lx *lexer) stateFn { r := lx.peek() switch { case r == eof: return lx.errorf("Un terminated map") case isKeySeparator(r): return lx.errorf("Unexpected key separator '%v'.", r) case isWhitespace(r) || isNL(r): lx.next() return lexSkip(lx, lexMapKeyStart) case r == mapEnd: lx.next() return lexSkip(lx, lexMapEnd) case r == commentHashStart: lx.next() lx.push(lexMapKeyStart) return lexCommentStart case r == commentSlashStart: lx.next() rn := lx.next() if rn == commentSlashStart { lx.push(lexMapKeyStart) return lexCommentStart } lx.backup() case r == sqStringStart: lx.next() return lexSkip(lx, lexMapQuotedKey) case r == dqStringStart: lx.next() return lexSkip(lx, lexMapDubQuotedKey) } lx.ignore() lx.next() return lexMapKey } // lexMapQuotedKey consumes the text of a key between quotes. func lexMapQuotedKey(lx *lexer) stateFn { r := lx.peek() if r == sqStringEnd { lx.emit(itemKey) lx.next() return lexSkip(lx, lexMapKeyEnd) } lx.next() return lexMapQuotedKey } // lexMapQuotedKey consumes the text of a key between quotes. func lexMapDubQuotedKey(lx *lexer) stateFn { r := lx.peek() if r == dqStringEnd { lx.emit(itemKey) lx.next() return lexSkip(lx, lexMapKeyEnd) } lx.next() return lexMapDubQuotedKey } // lexMapKey consumes the text of a key. Assumes that the first character (which // is not whitespace) has already been consumed. func lexMapKey(lx *lexer) stateFn { r := lx.peek() if isWhitespace(r) || isNL(r) || isKeySeparator(r) { lx.emit(itemKey) return lexMapKeyEnd } lx.next() return lexMapKey } // lexMapKeyEnd consumes the end of a key (up to the key separator). // Assumes that the first whitespace character after a key (or the '=' // separator) has NOT been consumed. func lexMapKeyEnd(lx *lexer) stateFn { r := lx.next() switch { case isWhitespace(r) || isNL(r): return lexSkip(lx, lexMapKeyEnd) case isKeySeparator(r): return lexSkip(lx, lexMapValue) } // We start the value here lx.backup() return lexMapValue } // lexMapValue consumes one value in a map. It assumes that '{' or ',' // have already been consumed. All whitespace and new lines are ignored. // Map values can be separated by ',' or simple NLs. func lexMapValue(lx *lexer) stateFn { r := lx.next() switch { case isWhitespace(r) || isNL(r): return lexSkip(lx, lexMapValue) case r == commentHashStart: lx.push(lexMapValue) return lexCommentStart case r == commentSlashStart: rn := lx.next() if rn == commentSlashStart { lx.push(lexMapValue) return lexCommentStart } lx.backup() fallthrough case r == mapValTerm: return lx.errorf("Unexpected map value terminator %q.", mapValTerm) case r == mapEnd: return lexSkip(lx, lexMapEnd) } lx.backup() lx.push(lexMapValueEnd) return lexValue } // lexMapValueEnd consumes the cruft between values of a map. Namely, // it ignores whitespace and expects either a ',' or a '}'. func lexMapValueEnd(lx *lexer) stateFn { r := lx.next() switch { case isWhitespace(r): return lexSkip(lx, lexMapValueEnd) case r == commentHashStart: lx.push(lexMapValueEnd) return lexCommentStart case r == commentSlashStart: rn := lx.next() if rn == commentSlashStart { lx.push(lexMapValueEnd) return lexCommentStart } lx.backup() fallthrough case r == optValTerm || r == mapValTerm || isNL(r): return lexSkip(lx, lexMapKeyStart) // Move onto next case r == mapEnd: return lexSkip(lx, lexMapEnd) } return lx.errorf("Expected a map value terminator %q or a map "+ "terminator %q, but got '%v' instead.", mapValTerm, mapEnd, r) } // lexMapEnd finishes the lexing of a map. It assumes that a '}' has // just been consumed. func lexMapEnd(lx *lexer) stateFn { lx.ignore() lx.emit(itemMapEnd) return lx.pop() } // Checks if the unquoted string was actually a boolean func (lx *lexer) isBool() bool { str := lx.input[lx.start:lx.pos] str = strings.ToLower(str) return str == "true" || str == "false" } // lexQuotedString consumes the inner contents of a string. It assumes that the // beginning '"' has already been consumed and ignored. It will not interpret any // internal contents. func lexQuotedString(lx *lexer) stateFn { r := lx.next() switch { case r == sqStringEnd: lx.backup() lx.emit(itemString) lx.next() lx.ignore() return lx.pop() } return lexQuotedString } // lexDubQuotedString consumes the inner contents of a string. It assumes that the // beginning '"' has already been consumed and ignored. It will not interpret any // internal contents. func lexDubQuotedString(lx *lexer) stateFn { r := lx.next() switch { case r == dqStringEnd: lx.backup() lx.emit(itemString) lx.next() lx.ignore() return lx.pop() } return lexDubQuotedString } // lexString consumes the inner contents of a string. It assumes that the // beginning '"' has already been consumed and ignored. func lexString(lx *lexer) stateFn { r := lx.next() //u.Infof("lexString r = %v", string(r)) switch { case r == '\\': return lexStringEscape // Termination of non-quoted strings case lx.isEnd(lx, r): lx.backup() if lx.isBool() { lx.emit(itemBool) } else { lx.emit(itemString) } return lx.pop() case r == sqStringEnd: lx.backup() lx.emit(itemString) lx.next() lx.ignore() return lx.pop() } return lexString } // lexDubString consumes the inner contents of a string. It assumes that the // beginning '"' has already been consumed and ignored. func lexDubString(lx *lexer) stateFn { r := lx.next() switch { case r == '\\': return lexStringEscape // Termination of non-quoted strings case isNL(r) || r == eof || r == optValTerm || isWhitespace(r): lx.backup() if lx.isBool() { lx.emit(itemBool) } else { lx.emit(itemString) } return lx.pop() case r == dqStringEnd: lx.backup() lx.emit(itemString) lx.next() lx.ignore() return lx.pop() } return lexDubString } // lexBlock consumes the inner contents as a string. It assumes that the // beginning '(' has already been consumed and ignored. It will continue // processing until it finds a ')' on a new line by itself. func lexBlock(lx *lexer) stateFn { r := lx.next() //u.Debugf("lexBlock() pos=%d len=%d %q", lx.pos, len(lx.input), lx.input[lx.pos:]) switch { case r == blockEnd: lx.backup() // unconsume ) we are going to verify below lx.backup() // unconsume previous rune to ensure it is newline // Looking for a ')' character on a line by itself, if the previous // character isn't a new line, then break so we keep processing the block. if lx.next() != '\n' { lx.next() // if inline ( this will consume it break } lx.next() // Make sure the next character is a new line or an eof. We want a ')' on a // bare line by itself. switch r = lx.next(); r { case '\n', eof: if r == eof { lx.width = 1 } lx.backup() // unconsume the \n, or eof r = lx.peek() if r != ')' { // For some reason on EOF we aren't where we think lx.backup() // unconsume the ) } lx.backup() // unconsume the \n lx.emit(itemString) lx.next() // consume the previous line \n lx.next() // consume the ) lx.ignore() return lx.pop() } lx.backup() } return lexBlock } // lexStringEscape consumes an escaped character. It assumes that the preceding // '\\' has already been consumed. func lexStringEscape(lx *lexer) stateFn { r := lx.next() switch r { case 'x': return lexStringBinary case 't': fallthrough case 'n': fallthrough case 'r': fallthrough case '"': fallthrough case '\\': return lexString } return lx.errorf("Invalid escape character '%v'. Only the following "+ "escape characters are allowed: \\xXX, \\t, \\n, \\r, \\\", \\\\.", r) } // lexStringBinary consumes two hexadecimal digits following '\x'. It assumes // that the '\x' has already been consumed. func lexStringBinary(lx *lexer) stateFn { r := lx.next() if !isHexadecimal(r) { return lx.errorf("Expected two hexadecimal digits after '\\x', but "+ "got '%v' instead.", r) } r = lx.next() if !isHexadecimal(r) { return lx.errorf("Expected two hexadecimal digits after '\\x', but "+ "got '%v' instead.", r) } return lexString } // lexNumberOrDateStart consumes either a (positive) integer, float or datetime. // It assumes that NO negative sign has been consumed. func lexNumberOrDateStart(lx *lexer) stateFn { r := lx.next() if !isDigit(r) { if r == '.' { return lx.errorf("Floats must start with a digit, not '.'.") } else { return lx.errorf("Expected a digit but got '%v'.", r) } } return lexNumberOrDate } // lexNumberOrDate consumes either a (positive) integer, float or datetime. func lexNumberOrDate(lx *lexer) stateFn { r := lx.next() switch { case r == '-': if lx.pos-lx.start != 5 { return lx.errorf("All ISO8601 dates must be in full Zulu form.") } return lexDateAfterYear case isDigit(r): return lexNumberOrDate case r == '.': return lexFloatStart } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexDateAfterYear consumes a full Zulu Datetime in ISO8601 format. // It assumes that "YYYY-" has already been consumed. func lexDateAfterYear(lx *lexer) stateFn { formats := []rune{ // digits are '0'. // everything else is direct equality. '0', '0', '-', '0', '0', 'T', '0', '0', ':', '0', '0', ':', '0', '0', 'Z', } for _, f := range formats { r := lx.next() if f == '0' { if !isDigit(r) { return lx.errorf("Expected digit in ISO8601 datetime, "+ "but found '%v' instead.", r) } } else if f != r { return lx.errorf("Expected '%v' in ISO8601 datetime, "+ "but found '%v' instead.", f, r) } } lx.emit(itemDatetime) return lx.pop() } // lexNumberStart consumes either an integer or a float. It assumes that a // negative sign has already been read, but that *no* digits have been consumed. // lexNumberStart will move to the appropriate integer or float states. func lexNumberStart(lx *lexer) stateFn { // we MUST see a digit. Even floats have to start with a digit. r := lx.next() if !isDigit(r) { if r == '.' { return lx.errorf("Floats must start with a digit, not '.'.") } else { return lx.errorf("Expected a digit but got '%v'.", r) } } return lexNumber } // lexNumber consumes an integer or a float after seeing the first digit. func lexNumber(lx *lexer) stateFn { r := lx.next() switch { case isDigit(r): return lexNumber case r == '.': return lexFloatStart } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexFloatStart starts the consumption of digits of a float after a '.'. // Namely, at least one digit is required. func lexFloatStart(lx *lexer) stateFn { r := lx.next() if !isDigit(r) { return lx.errorf("Floats must have a digit after the '.', but got "+ "'%v' instead.", r) } return lexFloat } // lexFloat consumes the digits of a float after a '.'. // Assumes that one digit has been consumed after a '.' already. func lexFloat(lx *lexer) stateFn { r := lx.next() if isDigit(r) { return lexFloat } lx.backup() lx.emit(itemFloat) return lx.pop() } // lexCommentStart begins the lexing of a comment. It will emit // itemCommentStart and consume no characters, passing control to lexComment. func lexCommentStart(lx *lexer) stateFn { lx.ignore() lx.emit(itemCommentStart) return lexComment } // lexComment lexes an entire comment. It assumes that '#' has been consumed. // It will consume *up to* the first new line character, and pass control // back to the last state on the stack. func lexComment(lx *lexer) stateFn { r := lx.peek() if isNL(r) || r == eof { lx.emit(itemText) return lx.pop() } lx.next() return lexComment } // lexSkip ignores all slurped input and moves on to the next state. func lexSkip(lx *lexer, nextState stateFn) stateFn { return func(lx *lexer) stateFn { lx.ignore() return nextState } } func isEndNormal(lx *lexer, r rune) bool { return (isNL(r) || r == eof || r == optValTerm || isWhitespace(r)) } func isEndArrayUnQuoted(lx *lexer, r rune) bool { return (isNL(r) || r == eof || r == optValTerm || r == arrayEnd || r == arrayValTerm || isWhitespace(r)) } func isIdentifierRune(r rune) bool { if unicode.IsLetter(r) || unicode.IsDigit(r) { return true } for _, allowedRune := range IdentityChars { if allowedRune == r { return true } } return false } // Tests for both key separators func isKeySeparator(r rune) bool { return r == keySepEqual || r == keySepColon } // isWhitespace returns true if `r` is a whitespace character according // to the spec. func isWhitespace(r rune) bool { return r == '\t' || r == ' ' } func isNL(r rune) bool { return r == '\n' || r == '\r' } func isDigit(r rune) bool { return r >= '0' && r <= '9' } func isHexadecimal(r rune) bool { return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') } func (item item) String() string { return fmt.Sprintf("(%T, '%s', %d)", item.typ, item.val, item.line) } func escapeSpecial(c rune) string { switch c { case '\n': return "\\n" } return string(c) } ================================================ FILE: lex_test.go ================================================ package confl import ( "testing" u "github.com/araddon/gou" ) var _ = u.EMPTY // Test to make sure we get what we expect. func expect(t *testing.T, lx *lexer, items []item) { for i := 0; i < len(items); i++ { item := lx.nextItem() if item.typ == itemEOF { break } else if item.typ == itemError { t.Fatal(item.val) } if item != items[i] { //u.Debugf("\n\n%s wanted: \n%s\ngot: \n%s", label, wantStr, got) wantStr := items[i].val got := item.val for pos, r := range wantStr { if len(got)-1 < pos { u.Warnf("len mismatch? %v vs %v", len(got), len(wantStr)) } else if r != rune(got[pos]) { u.Warnf("mismatch at position: %v %q!=%q", pos, string(r), string(got[pos])) break } else { //u.Debugf("match at position: %v %q=%q", pos, string(r), string(got[pos])) } } t.Fatalf("Testing: '%s'\nExpected %q, received %q\n", lx.input, items[i], item) } } } // Test to make sure we get what we expect. func expectError(t *testing.T, conf string) { lx := lex(conf) for { item := lx.nextItem() if item.typ == itemEOF { t.Fatalf("expected error got EOF for %v", conf) break } else if item.typ == itemError { // Success return } } } var errorVals = []string{ `//comment1 config : { / -- bad comment1 }`, `//comment2 config : { port: 8080 }/ -- bad comment2`, `/comment3 port: 8080`, `port: 8083 /badcomment4`, `port: 8084 //goodcomment1 portx: 80/badcomment4`, `:port: 8085`, // Can't start key with key seperator `=port=8086`, // Can't start key with key seperator `rate=.55`, // can't start values with . `rate= `, // can't start values with new line. } func TestLexErrors(t *testing.T) { for _, v := range errorVals { expectError(t, v) } } func TestLexBadConfString(t *testing.T) { lx := lex("\x0e9\xbd\xbf\xefr") item := lx.nextItem() if item.typ != itemError { t.Fatal(item.val) } } func TestLexSimpleKeyStringValues(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemString, "bar", 1}, {itemEOF, "", 1}, } // Double quotes lx := lex("foo = \"bar\"") expect(t, lx, expectedItems) // Single quotes lx = lex("foo = 'bar'") expect(t, lx, expectedItems) // No spaces lx = lex("foo='bar'") expect(t, lx, expectedItems) // NL lx = lex("foo='bar'\r\n") expect(t, lx, expectedItems) } func TestLexSimpleKeyIntegerValues(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemInteger, "123", 1}, {itemEOF, "", 1}, } lx := lex("foo = 123") expect(t, lx, expectedItems) lx = lex("foo=123") expect(t, lx, expectedItems) lx = lex("foo=123\r\n") expect(t, lx, expectedItems) } func TestLexSimpleKeyFloatValues(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemFloat, "22.2", 1}, {itemEOF, "", 1}, } lx := lex("foo = 22.2") expect(t, lx, expectedItems) lx = lex("foo=22.2") expect(t, lx, expectedItems) lx = lex("foo=22.2\r\n") expect(t, lx, expectedItems) } func TestLexSimpleKeyBoolValues(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemBool, "true", 1}, {itemEOF, "", 1}, } lx := lex("foo = true") expect(t, lx, expectedItems) lx = lex("foo=true") expect(t, lx, expectedItems) lx = lex("foo=true\r\n") expect(t, lx, expectedItems) lx = lex("foo=True") expect(t, lx, []item{ {itemKey, "foo", 1}, {itemBool, "True", 1}, {itemEOF, "", 1}, }) } func TestLexComments(t *testing.T) { expectedItems := []item{ {itemCommentStart, "", 1}, {itemText, " This is a comment", 1}, {itemEOF, "", 1}, } lx := lex("# This is a comment") expect(t, lx, expectedItems) lx = lex("# This is a comment\r\n") expect(t, lx, expectedItems) lx = lex("// This is a comment\r\n") expect(t, lx, expectedItems) } func TestLexArrays(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemArrayStart, "", 1}, {itemInteger, "1", 1}, {itemInteger, "2", 1}, {itemInteger, "3", 1}, {itemString, "bar", 1}, {itemArrayEnd, "", 1}, {itemEOF, "", 1}, } lx := lex("foo = [1, 2, 3, 'bar']") expect(t, lx, expectedItems) lx = lex("foo = [1,2,3,'bar']") expect(t, lx, expectedItems) lx = lex("foo = [1, 2,3,'bar']") expect(t, lx, expectedItems) } var mlArray = ` # top level comment foo = [ 1, # One 2, // Two 3 , // Three 'bar' , "bar" ] ` func TestLexMultilineArrays(t *testing.T) { expectedItems := []item{ {itemCommentStart, "", 2}, {itemText, " top level comment", 2}, {itemKey, "foo", 3}, {itemArrayStart, "", 3}, {itemInteger, "1", 4}, {itemCommentStart, "", 4}, {itemText, " One", 4}, {itemInteger, "2", 5}, {itemCommentStart, "", 5}, {itemText, " Two", 5}, {itemInteger, "3", 6}, {itemCommentStart, "", 6}, {itemText, " Three", 6}, {itemString, "bar", 7}, {itemString, "bar", 8}, {itemArrayEnd, "", 9}, {itemEOF, "", 9}, } lx := lex(mlArray) expect(t, lx, expectedItems) } func TestLexUnterminated(t *testing.T) { lx := lex("foo {\n name = 'bill' \n") lx.nextItem() // foo lx.nextItem() // map start = "" lx.nextItem() // name lx.nextItem() // bill item := lx.nextItem() if item.typ != itemError { t.Errorf("Should be error for un terminated map: %v", item) } } var mlArrayNoSep = ` # top level comment foo = [ 1 2 3 'bar' "bar" ] ` func TestLexMultilineArraysNoSep(t *testing.T) { expectedItems := []item{ {itemCommentStart, "", 2}, {itemText, " top level comment", 2}, {itemKey, "foo", 3}, {itemArrayStart, "", 3}, {itemInteger, "1", 4}, {itemInteger, "2", 5}, {itemInteger, "3", 6}, {itemString, "bar", 7}, {itemString, "bar", 8}, {itemArrayEnd, "", 9}, {itemEOF, "", 9}, } lx := lex(mlArrayNoSep) expect(t, lx, expectedItems) } func TestLexSimpleMap(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemMapStart, "", 1}, {itemKey, "ip", 1}, {itemString, "127.0.0.1", 1}, {itemKey, "port", 1}, {itemInteger, "4242", 1}, {itemMapEnd, "", 1}, {itemEOF, "", 1}, } lx := lex("foo = {ip='127.0.0.1', port = 4242}") expect(t, lx, expectedItems) } var mlMap = ` foo = { ip = '127.0.0.1' # comment1 #comment2 port= 4242 // comment3 rate = 55 } ` func TestLexMultilineMap(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 2}, {itemMapStart, "", 2}, {itemKey, "ip", 3}, {itemString, "127.0.0.1", 3}, {itemCommentStart, "", 4}, {itemText, " comment1", 4}, {itemCommentStart, "", 5}, {itemText, "comment2", 5}, {itemKey, "port", 6}, {itemInteger, "4242", 6}, {itemCommentStart, "", 7}, {itemText, " comment3", 7}, {itemKey, "rate", 8}, {itemInteger, "55", 8}, {itemMapEnd, "", 9}, {itemEOF, "", 9}, } lx := lex(mlMap) expect(t, lx, expectedItems) } var nestedMap = ` foo = { host = { ip = '127.0.0.1' port= 4242 // rate comment rate = 55 } } ` func TestLexNestedMaps(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 2}, {itemMapStart, "", 2}, {itemKey, "host", 3}, {itemMapStart, "", 3}, {itemKey, "ip", 4}, {itemString, "127.0.0.1", 4}, {itemKey, "port", 5}, {itemInteger, "4242", 5}, {itemCommentStart, "", 6}, {itemText, " rate comment", 6}, {itemKey, "rate", 7}, {itemInteger, "55", 7}, {itemMapEnd, "", 8}, {itemMapEnd, "", 9}, {itemEOF, "", 5}, } lx := lex(nestedMap) expect(t, lx, expectedItems) } func TestLexQuotedKeys(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemInteger, "123", 1}, {itemEOF, "", 1}, } lx := lex("foo : 123") expect(t, lx, expectedItems) lx = lex("'foo' : 123") expect(t, lx, expectedItems) lx = lex("\"foo\" : 123") expect(t, lx, expectedItems) } func TestLexQuotedKeysWithSpace(t *testing.T) { expectedItems := []item{ {itemKey, " foo", 1}, {itemInteger, "123", 1}, {itemEOF, "", 1}, } lx := lex("' foo' : 123") expect(t, lx, expectedItems) lx = lex("\" foo\" : 123") expect(t, lx, expectedItems) } func TestLexColonKeySep(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemInteger, "123", 1}, {itemEOF, "", 1}, } lx := lex("foo : 123") expect(t, lx, expectedItems) lx = lex("foo:123") expect(t, lx, expectedItems) lx = lex("foo: 123") expect(t, lx, expectedItems) lx = lex("foo: 123\r\n") expect(t, lx, expectedItems) } func TestLexWhitespaceKeySep(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemInteger, "123", 1}, {itemEOF, "", 1}, } lx := lex("foo 123") expect(t, lx, expectedItems) lx = lex("foo 123") expect(t, lx, expectedItems) lx = lex("foo\t123") expect(t, lx, expectedItems) lx = lex("foo\t\t123\r\n") expect(t, lx, expectedItems) } var nestedWhitespaceMap = ` foo { host { ip = '127.0.0.1' port= 4242 } } ` func TestLexNestedWhitespaceMaps(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 2}, {itemMapStart, "", 2}, {itemKey, "host", 3}, {itemMapStart, "", 3}, {itemKey, "ip", 4}, {itemString, "127.0.0.1", 4}, {itemKey, "port", 5}, {itemInteger, "4242", 5}, {itemMapEnd, "", 6}, {itemMapEnd, "", 7}, {itemEOF, "", 5}, } lx := lex(nestedWhitespaceMap) expect(t, lx, expectedItems) } // Not currently supported var tableOfArrays = ` table [ [ 1, 2], [ "a", "b"], ] ` func TestLexTableOfArrays(t *testing.T) { expectedItems := []item{ {itemKey, "table", 2}, {itemArrayStart, "", 2}, {itemArrayStart, "", 3}, {itemInteger, "1", 3}, {itemInteger, "2", 3}, {itemArrayEnd, "", 3}, {itemArrayStart, "", 4}, {itemString, "a", 4}, {itemString, "b", 4}, {itemArrayEnd, "", 4}, {itemArrayEnd, "", 5}, {itemEOF, "", 6}, } lx := lex(tableOfArrays) expect(t, lx, expectedItems) } var semicolons = ` foo = 123; bar = 'baz'; baz = 'boo' map { id = 1; } ` func TestOptionalSemicolons(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 2}, {itemInteger, "123", 2}, {itemKey, "bar", 3}, {itemString, "baz", 3}, {itemKey, "baz", 4}, {itemString, "boo", 4}, {itemKey, "map", 5}, {itemMapStart, "", 5}, {itemKey, "id", 6}, {itemInteger, "1", 6}, {itemMapEnd, "", 7}, {itemEOF, "", 5}, } lx := lex(semicolons) expect(t, lx, expectedItems) } func TestLexSemicolonChaining(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemString, "1", 1}, {itemKey, "bar", 1}, {itemFloat, "2.2", 1}, {itemKey, "baz", 1}, {itemBool, "true", 1}, {itemEOF, "", 1}, } lx := lex("foo='1'; bar=2.2; baz=true;") expect(t, lx, expectedItems) } var noquotes = ` foo = 123 bar = baz baz=boo map { id:one id2 : onetwo } t true f false tstr "true" tkey = two fkey = five # This should be a string ` func TestLexNonQuotedStrings(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 2}, {itemInteger, "123", 2}, {itemKey, "bar", 3}, {itemString, "baz", 3}, {itemKey, "baz", 4}, {itemString, "boo", 4}, {itemKey, "map", 5}, {itemMapStart, "", 5}, {itemKey, "id", 6}, {itemString, "one", 6}, {itemKey, "id2", 7}, {itemString, "onetwo", 7}, {itemMapEnd, "", 8}, {itemKey, "t", 9}, {itemBool, "true", 9}, {itemKey, "f", 10}, {itemBool, "false", 10}, {itemKey, "tstr", 11}, {itemString, "true", 11}, {itemKey, "tkey", 12}, {itemString, "two", 12}, {itemKey, "fkey", 13}, {itemString, "five", 13}, {itemCommentStart, "", 13}, {itemText, " This should be a string", 13}, {itemEOF, "", 14}, } lx := lex(noquotes) expect(t, lx, expectedItems) } func TestLexMapQuotedKeys(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemMapStart, "", 1}, {itemKey, "bar", 1}, {itemInteger, "4242", 1}, {itemMapEnd, "", 1}, {itemEOF, "", 1}, } lx := lex("foo = {'bar' = 4242}") expect(t, lx, expectedItems) lx = lex("foo = {\"bar\" = 4242}") expect(t, lx, expectedItems) } func TestLexSpecialCharsMapQuotedKeys(t *testing.T) { expectedItems := []item{ {itemKey, "foo", 1}, {itemMapStart, "", 1}, {itemKey, "bar-1.2.3", 1}, {itemMapStart, "", 1}, {itemKey, "port", 1}, {itemInteger, "4242", 1}, {itemMapEnd, "", 1}, {itemMapEnd, "", 1}, {itemEOF, "", 1}, } lx := lex("foo = {'bar-1.2.3' = { port:4242 }}") expect(t, lx, expectedItems) lx = lex("foo = {\"bar-1.2.3\" = { port:4242 }}") expect(t, lx, expectedItems) } var mlnestedmap = ` systems { allinone { description: "This is a description." } } ` func TestLexDoubleNestedMapsNewLines(t *testing.T) { expectedItems := []item{ {itemKey, "systems", 2}, {itemMapStart, "", 2}, {itemKey, "allinone", 3}, {itemMapStart, "", 3}, {itemKey, "description", 4}, {itemString, "This is a description.", 4}, {itemMapEnd, "", 5}, {itemMapEnd, "", 6}, {itemEOF, "", 7}, } lx := lex(mlnestedmap) expect(t, lx, expectedItems) } var blockexample = ` numbers ( 1234567890 ) ` func TestLexBlockString(t *testing.T) { expectedItems := []item{ {itemKey, "numbers", 2}, {itemString, "1234567890", 3}, } lx := lex(blockexample) expect(t, lx, expectedItems) } func TestLexBlockStringEOF(t *testing.T) { expectedItems := []item{ {itemKey, "numbers", 2}, {itemString, "1234567890", 3}, } blockbytes := []byte(blockexample[0 : len(blockexample)-1]) blockbytes = append(blockbytes, 0) lx := lex(string(blockbytes)) expect(t, lx, expectedItems) } var mlblockexample = ` numbers ( 12(34)56 ( 7890 ) ) ` var mlBlockTextVal = ` 12(34)56 ( 7890 )` func TestLexBlockStringMultiLine(t *testing.T) { expectedItems := []item{ {itemKey, "numbers", 2}, {itemString, mlBlockTextVal, 6}, } lx := lex(mlblockexample) expect(t, lx, expectedItems) } ================================================ FILE: parse.go ================================================ // Copyright 2013 Apcera Inc. All rights reserved. // Conf is a configuration file format used by gnatsd. It is // a flexible format that combines the best of traditional // configuration formats and newer styles such as JSON and YAML. package confl // The format supported is less restrictive than today's formats. // Supports mixed Arrays [], nested Maps {}, multiple comment types (# and //) // Also supports key value assigments using '=' or ':' or whiteSpace() // e.g. foo = 2, foo : 2, foo 2 // maps can be assigned with no key separator as well // semicolons as value terminators in key/value assignments are optional // // see parse_test.go for more examples. import ( "fmt" "log" "strconv" "strings" "time" "unicode/utf8" u "github.com/araddon/gou" ) var _ = u.EMPTY // Parser will return a map of keys to interface{}, although concrete types // underly them. The values supported are string, bool, int64, float64, DateTime. // Arrays and nested Maps are also supported. type parser struct { mapping map[string]interface{} types map[string]confType lx *lexer // A list of keys in the order that they appear in the data. ordered []Key // the full key for the current hash in scope context Key // the base key name for everything except hashes currentKey string // rough approximation of line number approxLine int // The current scoped context, can be array or map ctx interface{} // stack of contexts, either map or array/slice stack ctxs []interface{} // Keys stack keys []string // A map of 'key.group.names' to whether they were created implicitly. implicits map[string]bool } type parseError string func (pe parseError) Error() string { return string(pe) } func Parse(data string) (map[string]interface{}, error) { p, err := parse(data) if err != nil { return nil, err } return p.mapping, nil } func parse(data string) (p *parser, err error) { p = &parser{ mapping: make(map[string]interface{}), lx: lex(data), ctxs: make([]interface{}, 0, 4), keys: make([]string, 0, 4), } p.pushContext(p.mapping) for { it := p.next() if it.typ == itemEOF { break } if err := p.processItem(it); err != nil { return nil, err } } return p, nil } func (p *parser) panicf(format string, v ...interface{}) { msg := fmt.Sprintf("Near line %d, key '%s': %s", p.approxLine, p.current(), fmt.Sprintf(format, v...)) panic(parseError(msg)) } func (p *parser) next() item { return p.lx.nextItem() } func (p *parser) bug(format string, v ...interface{}) { log.Fatalf("BUG: %s\n\n", fmt.Sprintf(format, v...)) } func (p *parser) expect(typ itemType) item { it := p.next() p.assertEqual(typ, it.typ) return it } func (p *parser) assertEqual(expected, got itemType) { if expected != got { p.bug("Expected '%s' but got '%s'.", expected, got) } } func (p *parser) pushContext(ctx interface{}) { p.ctxs = append(p.ctxs, ctx) p.ctx = ctx } func (p *parser) popContext() interface{} { if len(p.ctxs) == 0 { panic("BUG in parser, context stack empty") } li := len(p.ctxs) - 1 last := p.ctxs[li] p.ctxs = p.ctxs[0:li] p.ctx = p.ctxs[len(p.ctxs)-1] return last } func (p *parser) pushKey(key string) { p.keys = append(p.keys, key) } func (p *parser) popKey() string { if len(p.keys) == 0 { panic("BUG in parser, keys stack empty") } li := len(p.keys) - 1 last := p.keys[li] p.keys = p.keys[0:li] return last } func (p *parser) processItem(it item) error { switch it.typ { case itemError: //panic("error") return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val) case itemKey: p.pushKey(it.val) case itemMapStart: newCtx := make(map[string]interface{}) p.pushContext(newCtx) case itemMapEnd: p.setValue(p.popContext()) case itemString: // FIXME(dlc) sanitize string? p.setValue(maybeRemoveIndents(it.val)) case itemInteger: num, err := strconv.ParseInt(it.val, 10, 64) if err != nil { if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { return fmt.Errorf("Integer '%s' is out of the range.", it.val) } else { return fmt.Errorf("Expected integer, but got '%s'.", it.val) } } p.setValue(num) case itemFloat: num, err := strconv.ParseFloat(it.val, 64) if err != nil { if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { return fmt.Errorf("Float '%s' is out of the range.", it.val) } else { return fmt.Errorf("Expected float, but got '%s'.", it.val) } } p.setValue(num) case itemBool: switch it.val { case "true": p.setValue(true) case "false": p.setValue(false) default: return fmt.Errorf("Expected boolean value, but got '%s'.", it.val) } case itemDatetime: dt, err := time.Parse("2006-01-02T15:04:05Z", it.val) if err != nil { return fmt.Errorf( "Expected Zulu formatted DateTime, but got '%s'.", it.val) } p.setValue(dt) case itemArrayStart: array := make([]interface{}, 0) p.pushContext(array) case itemArrayEnd: array := p.ctx p.popContext() p.setValue(array) } return nil } func (p *parser) setValue(val interface{}) { // Test to see if we are on an array or a map // Array processing if ctx, ok := p.ctx.([]interface{}); ok { p.ctx = append(ctx, val) p.ctxs[len(p.ctxs)-1] = p.ctx } // Map processing if ctx, ok := p.ctx.(map[string]interface{}); ok { key := p.popKey() // FIXME(dlc), make sure to error if redefining same key? ctx[key] = val } } // setType sets the type of a particular value at a given key. // It should be called immediately AFTER setValue. // // Note that if `key` is empty, then the type given will be applied to the // current context (which is either a table or an array of tables). func (p *parser) setType(key string, typ confType) { keyContext := make(Key, 0, len(p.context)+1) for _, k := range p.context { keyContext = append(keyContext, k) } if len(key) > 0 { // allow type setting for hashes keyContext = append(keyContext, key) } p.types[keyContext.String()] = typ } // addImplicit sets the given Key as having been created implicitly. func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = true } // removeImplicit stops tagging the given key as having been implicitly created. func (p *parser) removeImplicit(key Key) { p.implicits[key.String()] = false } // isImplicit returns true if the key group pointed to by the key was created // implicitly. func (p *parser) isImplicit(key Key) bool { return p.implicits[key.String()] } // current returns the full key name of the current context. func (p *parser) current() string { if len(p.currentKey) == 0 { return p.context.String() } if len(p.context) == 0 { return p.currentKey } return fmt.Sprintf("%s.%s", p.context, p.currentKey) } // for multi-line text comments lets remove the Indent func maybeRemoveIndents(s string) string { if !strings.Contains(s, "\n") { return s } lines := strings.Split(s, "\n") indent := 0 findIndent: for idx, r := range lines[0] { switch r { case '\t', ' ': // keep consuming default: // first non-whitespace we are going to break // and use this as indent size. This makes a variety of assumptions // - subsequent indents use same mixture of spaces/tabs indent = idx break findIndent } } for i, line := range lines { //u.Debugf("%v indent=%d line %q", i, indent, line) if len(line) >= indent { lines[i] = line[indent:] } } return strings.Join(lines, "\n") } func replaceEscapes(s string) string { return strings.NewReplacer( "\\b", "\u0008", "\\t", "\u0009", "\\n", "\u000A", "\\f", "\u000C", "\\r", "\u000D", "\\\"", "\u0022", "\\/", "\u002F", "\\\\", "\u005C", ).Replace(s) } func (p *parser) replaceUnicode(s string) string { indexEsc := func() int { return strings.Index(s, "\\u") } for i := indexEsc(); i != -1; i = indexEsc() { asciiBytes := s[i+2 : i+6] s = strings.Replace(s, s[i:i+6], p.asciiEscapeToUnicode(asciiBytes), -1) } return s } func (p *parser) asciiEscapeToUnicode(s string) string { hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32) if err != nil { p.bug("Could not parse '%s' as a hexadecimal number, but the "+ "lexer claims it's OK: %s", s, err) } // BUG(burntsushi) // I honestly don't understand how this works. I can't seem // to find a way to make this fail. I figured this would fail on invalid // UTF-8 characters like U+DCFF, but it doesn't. r := string(rune(hex)) if !utf8.ValidString(r) { p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s) } return string(r) } ================================================ FILE: parse_test.go ================================================ package confl import ( "fmt" "reflect" "testing" ) // Test to make sure we get what we expect. func test(t *testing.T, data string, ex map[string]interface{}) { m, err := Parse(data) if err != nil { t.Fatalf("Received err: %v\n", err) } if m == nil { t.Fatal("Received nil map") } if !reflect.DeepEqual(m, ex) { t.Fatalf("Not Equal:\nReceived: '%+v'\nExpected: '%+v'\n", m, ex) } } func TestParseSimpleTopLevel(t *testing.T) { ex := map[string]interface{}{ "foo": "1", "bar": float64(2.2), "baz": true, "boo": int64(22), } test(t, "foo='1'; bar=2.2; baz=true; boo=22", ex) } var sample1 = ` foo { host { ip = '127.0.0.1' port = 4242 } servers = [ "a.com", "b.com", "c.com"] } ` func TestParseSample1(t *testing.T) { ex := map[string]interface{}{ "foo": map[string]interface{}{ "host": map[string]interface{}{ "ip": "127.0.0.1", "port": int64(4242), }, "servers": []interface{}{"a.com", "b.com", "c.com"}, }, } test(t, sample1, ex) } var cluster = ` cluster { port: 4244 authorization { user: route_user password: top_secret timeout: 1 } # Routes are actively solicited and connected to from this server. # Other servers can connect to us if they supply the correct credentials # in their routes definitions from above. // Test both styles of comments routes = [ nats-route://foo:bar@apcera.me:4245 nats-route://foo:bar@apcera.me:4246 ] } ` func TestParseSample2(t *testing.T) { ex := map[string]interface{}{ "cluster": map[string]interface{}{ "port": int64(4244), "authorization": map[string]interface{}{ "user": "route_user", "password": "top_secret", "timeout": int64(1), }, "routes": []interface{}{ "nats-route://foo:bar@apcera.me:4245", "nats-route://foo:bar@apcera.me:4246", }, }, } test(t, cluster, ex) } var sample3 = ` foo { expr = '(true == "false")' text = 'This is a multi-line text block.' text2 ( hello world this is multi line with empty line ) } ` func TestParseSample3(t *testing.T) { ex := map[string]interface{}{ "foo": map[string]interface{}{ "expr": "(true == \"false\")", "text": "This is a multi-line\ntext block.", "text2": "hello world\n this is multi line\n\nwith empty line", }, } test(t, sample3, ex) } var sample4 = ` array [ { abc: 123 } { xyz: "word" } ] ` func TestParseSample4(t *testing.T) { ex := map[string]interface{}{ "array": []interface{}{ map[string]interface{}{"abc": int64(123)}, map[string]interface{}{"xyz": "word"}, }, } test(t, sample4, ex) } var sample5 = ` table [ [ 1, 123 ], [ "a", "b", "c"], ] ` func TestParseSample5(t *testing.T) { ex := map[string]interface{}{ "table": []interface{}{ []interface{}{int64(1), int64(123)}, []interface{}{"a", "b", "c"}, }, } test(t, sample5, ex) } func TestBigSlices(t *testing.T) { txt := "Hosts : [" for i := 0; i < 100; i++ { txt += fmt.Sprintf(`"http://192.168.1.%d:9999", `, i) } txt += `"http://123.123.123.123:9999"]` + "\n" x := struct{ Hosts []string }{} if err := Unmarshal([]byte(txt), &x); err != nil { t.Fatalf("error unmarshaling sample: %v", err) } if len(x.Hosts) != 101 { t.Fatalf("%d != 101", len(x.Hosts)) } for i, v := range x.Hosts { if i < 100 && v != fmt.Sprintf("http://192.168.1.%d:9999", i) { t.Errorf("%d unexpected: %s", i, v) } } if x.Hosts[100] != "http://123.123.123.123:9999" { t.Errorf("unexpected: %s", x.Hosts[100]) } } ================================================ FILE: type_check.go ================================================ package confl // represents any Go type that corresponds to a internal type. type confType interface { typeString() string } // typeEqual accepts any two types and returns true if they are equal. func typeEqual(t1, t2 confType) bool { if t1 == nil || t2 == nil { return false } return t1.typeString() == t2.typeString() } func typeIsHash(t confType) bool { return typeEqual(t, confHash) || typeEqual(t, confArrayHash) } type confBaseType string func (btype confBaseType) typeString() string { return string(btype) } func (btype confBaseType) String() string { return btype.typeString() } var ( confInteger confBaseType = "Integer" confFloat confBaseType = "Float" confDatetime confBaseType = "Datetime" confString confBaseType = "String" confBool confBaseType = "Bool" confArray confBaseType = "Array" confHash confBaseType = "Hash" confArrayHash confBaseType = "ArrayHash" ) // typeOfPrimitive returns a confType of any primitive value in conf. // Primitive values are: Integer, Float, Datetime, String and Bool. // // Passing a lexer item other than the following will cause a BUG message // to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime. func (p *parser) typeOfPrimitive(lexItem item) confType { switch lexItem.typ { case itemInteger: return confInteger case itemFloat: return confFloat case itemDatetime: return confDatetime case itemString: return confString case itemBool: return confBool } p.bug("Cannot infer primitive type of lex item '%s'.", lexItem) panic("unreachable") } // typeOfArray returns a confType for an array given a list of types of its // values. // // In the current spec, if an array is homogeneous, then its type is always // "Array". If the array is not homogeneous, an error is generated. func (p *parser) typeOfArray(types []confType) confType { // Empty arrays are cool. if len(types) == 0 { return confArray } theType := types[0] for _, t := range types[1:] { if !typeEqual(theType, t) { p.panicf("Array contains values of type '%s' and '%s', but arrays "+ "must be homogeneous.", theType, t) } } return confArray } ================================================ FILE: type_fields.go ================================================ package confl // Struct field handling is adapted from code in encoding/json: // // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the Go distribution. import ( "reflect" "sort" "strings" "sync" ) // A field represents a single field found in a struct. type field struct { name string // the name of the field (`confl` tag included) tag bool // whether field has a `confl` tag index []int // represents the depth of an anonymous field typ reflect.Type // the type of the field } // byName sorts field by name, breaking ties with depth, // then breaking ties with "name came from confl tag", then // breaking ties with index sequence. type byName []field func (x byName) Len() int { return len(x) } func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x byName) Less(i, j int) bool { if x[i].name != x[j].name { return x[i].name < x[j].name } if len(x[i].index) != len(x[j].index) { return len(x[i].index) < len(x[j].index) } if x[i].tag != x[j].tag { return x[i].tag } return byIndex(x).Less(i, j) } // byIndex sorts field by index sequence. type byIndex []field func (x byIndex) Len() int { return len(x) } func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x byIndex) Less(i, j int) bool { for k, xik := range x[i].index { if k >= len(x[j].index) { return false } if xik != x[j].index[k] { return xik < x[j].index[k] } } return len(x[i].index) < len(x[j].index) } // typeFields returns a list of fields that confl should recognize for the given // type. The algorithm is breadth-first search over the set of structs to // include - the top struct and then any reachable anonymous structs. func typeFields(t reflect.Type) []field { // Anonymous fields to explore at the current level and the next. current := []field{} next := []field{{typ: t}} // Count of queued names for current level and the next. count := map[reflect.Type]int{} nextCount := map[reflect.Type]int{} // Types already visited at an earlier level. visited := map[reflect.Type]bool{} // Fields found. var fields []field for len(next) > 0 { current, next = next, current[:0] count, nextCount = nextCount, map[reflect.Type]int{} for _, f := range current { if visited[f.typ] { continue } visited[f.typ] = true // Scan f.typ for fields to include. for i := 0; i < f.typ.NumField(); i++ { sf := f.typ.Field(i) if sf.PkgPath != "" { // unexported continue } name := sf.Tag.Get("confl") if name == "-" { continue } else if name != "" { // ClientID string `confl:"Client_id,omitempty"` parts := strings.Split(name, ",") if len(parts) > 1 { name = parts[0] } } if name == "" { name = sf.Tag.Get("json") if name == "-" { continue } // ClientID string `json:"Client_id,omitempty"` parts := strings.Split(name, ",") if len(parts) > 1 { name = parts[0] } } index := make([]int, len(f.index)+1) copy(index, f.index) index[len(f.index)] = i ft := sf.Type if ft.Name() == "" && ft.Kind() == reflect.Ptr { // Follow pointer. ft = ft.Elem() } // Record found field and index sequence. if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { tagged := name != "" if name == "" { name = sf.Name } fields = append(fields, field{name, tagged, index, ft}) if count[f.typ] > 1 { // If there were multiple instances, add a second, // so that the annihilation code will see a duplicate. // It only cares about the distinction between 1 or 2, // so don't bother generating any more copies. fields = append(fields, fields[len(fields)-1]) } continue } // Record new anonymous struct to explore in next round. nextCount[ft]++ if nextCount[ft] == 1 { f := field{name: ft.Name(), index: index, typ: ft} next = append(next, f) } } } } sort.Sort(byName(fields)) // Delete all fields that are hidden by the Go rules for embedded fields, // except that fields with tags are promoted. // The fields are sorted in primary order of name, secondary order // of field index length. Loop over names; for each name, delete // hidden fields by choosing the one dominant field that survives. out := fields[:0] for advance, i := 0, 0; i < len(fields); i += advance { // One iteration per name. // Find the sequence of fields with the name of this first field. fi := fields[i] name := fi.name for advance = 1; i+advance < len(fields); advance++ { fj := fields[i+advance] if fj.name != name { break } } if advance == 1 { // Only one field with this name out = append(out, fi) continue } dominant, ok := dominantField(fields[i : i+advance]) if ok { out = append(out, dominant) } } fields = out sort.Sort(byIndex(fields)) return fields } // dominantField looks through the fields, all of which are known to // have the same name, to find the single field that dominates the // others using Go's embedding rules, modified by the presence of // tags. If there are multiple top-level fields, the boolean // will be false: This condition is an error in Go and we skip all // the fields. func dominantField(fields []field) (field, bool) { // The fields are sorted in increasing index-length order. The winner // must therefore be one with the shortest index length. Drop all // longer entries, which is easy: just truncate the slice. length := len(fields[0].index) tagged := -1 // Index of first tagged field. for i, f := range fields { if len(f.index) > length { fields = fields[:i] break } if f.tag { if tagged >= 0 { // Multiple tagged fields at the same level: conflict. // Return no field. return field{}, false } tagged = i } } if tagged >= 0 { return fields[tagged], true } // All remaining fields have the same length. If there's more than one, // we have a conflict (two fields named "X" at the same level) and we // return no field. if len(fields) > 1 { return field{}, false } return fields[0], true } var fieldCache struct { sync.RWMutex m map[reflect.Type][]field } // cachedTypeFields is like typeFields but uses a cache to avoid repeated work. func cachedTypeFields(t reflect.Type) []field { fieldCache.RLock() f := fieldCache.m[t] fieldCache.RUnlock() if f != nil { return f } // Compute fields without lock. // Might duplicate effort but won't hold other computations back. f = typeFields(t) if f == nil { f = []field{} } fieldCache.Lock() if fieldCache.m == nil { fieldCache.m = map[reflect.Type][]field{} } fieldCache.m[t] = f fieldCache.Unlock() return f }