forked from mirror/ledisdb
Custom auth method (#301)
This commit is contained in:
parent
0b1b6de120
commit
74d1db9b35
|
@ -1,14 +0,0 @@
|
||||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
||||||
Version 2, December 2004
|
|
||||||
|
|
||||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
|
@ -1,505 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"math"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var e = fmt.Errorf
|
|
||||||
|
|
||||||
// Unmarshaler is the interface implemented by objects that can unmarshal a
|
|
||||||
// TOML description of themselves.
|
|
||||||
type Unmarshaler interface {
|
|
||||||
UnmarshalTOML(interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`.
|
|
||||||
func Unmarshal(p []byte, v interface{}) error {
|
|
||||||
_, err := Decode(string(p), v)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Primitive is a TOML 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 TOML data until run time.
|
|
||||||
type Primitive struct {
|
|
||||||
undecoded interface{}
|
|
||||||
context Key
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEPRECATED!
|
|
||||||
//
|
|
||||||
// Use MetaData.PrimitiveDecode instead.
|
|
||||||
func PrimitiveDecode(primValue Primitive, v interface{}) error {
|
|
||||||
md := MetaData{decoded: make(map[string]bool)}
|
|
||||||
return md.unify(primValue.undecoded, rvalue(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrimitiveDecode is just like the other `Decode*` functions, except it
|
|
||||||
// decodes a TOML 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode will decode the contents of `data` in TOML format into a pointer
|
|
||||||
// `v`.
|
|
||||||
//
|
|
||||||
// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be
|
|
||||||
// used interchangeably.)
|
|
||||||
//
|
|
||||||
// TOML arrays of tables correspond to either a slice of structs or a slice
|
|
||||||
// of maps.
|
|
||||||
//
|
|
||||||
// TOML datetimes correspond to Go `time.Time` values.
|
|
||||||
//
|
|
||||||
// All other TOML 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 TOML 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
|
|
||||||
//
|
|
||||||
// TOML keys can map to either keys in a Go map or field names in a Go
|
|
||||||
// struct. The special `toml` struct tag may be used to map TOML 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 TOML values and Go values is loose. That is, there
|
|
||||||
// may exist TOML values that cannot be placed into your representation, and
|
|
||||||
// there may be parts of your representation that do not correspond to
|
|
||||||
// TOML 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. Unmarshaler Interface support.
|
|
||||||
if rv.CanAddr() {
|
|
||||||
if v, ok := rv.Addr().Interface().(Unmarshaler); ok {
|
|
||||||
return v.UnmarshalTOML(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 TOML
|
|
||||||
// hash or array. In particular, the unmarshaler should only be applied
|
|
||||||
// to primitive TOML 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 {
|
|
||||||
if mapping == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
if tmap == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
if !datav.IsValid() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return badtype("slice", data)
|
|
||||||
}
|
|
||||||
sliceLen := datav.Len()
|
|
||||||
if sliceLen != rv.Len() {
|
|
||||||
return e("expected array length %d; got TOML 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 {
|
|
||||||
if !datav.IsValid() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return badtype("slice", data)
|
|
||||||
}
|
|
||||||
n := datav.Len()
|
|
||||||
if rv.IsNil() || rv.Cap() < n {
|
|
||||||
rv.Set(reflect.MakeSlice(rv.Type(), n, n))
|
|
||||||
}
|
|
||||||
rv.SetLen(n)
|
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// MetaData allows access to meta information about TOML data that may not
|
|
||||||
// be inferrable via reflection. In particular, whether a key has been defined
|
|
||||||
// and the TOML type of a key.
|
|
||||||
type MetaData struct {
|
|
||||||
mapping map[string]interface{}
|
|
||||||
types map[string]tomlType
|
|
||||||
keys []Key
|
|
||||||
decoded map[string]bool
|
|
||||||
context Key // Used only during decoding.
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDefined returns true if the key given exists in the TOML data. The key
|
|
||||||
// should be specified hierarchially. e.g.,
|
|
||||||
//
|
|
||||||
// // access the TOML 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 TOML 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) maybeQuotedAll() string {
|
|
||||||
var ss []string
|
|
||||||
for i := range k {
|
|
||||||
ss = append(ss, k.maybeQuoted(i))
|
|
||||||
}
|
|
||||||
return strings.Join(ss, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Key) maybeQuoted(i int) string {
|
|
||||||
quote := false
|
|
||||||
for _, c := range k[i] {
|
|
||||||
if !isBareKeyChar(c) {
|
|
||||||
quote = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if quote {
|
|
||||||
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
|
|
||||||
} else {
|
|
||||||
return k[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Key) add(piece string) Key {
|
|
||||||
newKey := make(Key, len(k)+1)
|
|
||||||
copy(newKey, k)
|
|
||||||
newKey[len(k)] = piece
|
|
||||||
return newKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns a slice of every key in the TOML 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 TOML 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 TOML 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 TOML 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
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
/*
|
|
||||||
Package toml provides facilities for decoding and encoding TOML configuration
|
|
||||||
files via reflection. There is also support for delaying decoding with
|
|
||||||
the Primitive type, and querying the set of keys in a TOML document with the
|
|
||||||
MetaData type.
|
|
||||||
|
|
||||||
The specification implemented: https://github.com/mojombo/toml
|
|
||||||
|
|
||||||
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
|
|
||||||
whether a file is a valid TOML document. It can also be used to print the
|
|
||||||
type of each key in a TOML document.
|
|
||||||
|
|
||||||
Testing
|
|
||||||
|
|
||||||
There are two important types of tests used for this package. The first is
|
|
||||||
contained inside '*_test.go' files and uses the standard Go unit testing
|
|
||||||
framework. These tests are primarily devoted to holistically testing the
|
|
||||||
decoder and encoder.
|
|
||||||
|
|
||||||
The second type of testing is used to verify the implementation's adherence
|
|
||||||
to the TOML specification. These tests have been factored into their own
|
|
||||||
project: https://github.com/BurntSushi/toml-test
|
|
||||||
|
|
||||||
The reason the tests are in a separate project is so that they can be used by
|
|
||||||
any implementation of TOML. Namely, it is language agnostic.
|
|
||||||
*/
|
|
||||||
package toml
|
|
|
@ -1,549 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tomlEncodeError 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(
|
|
||||||
"TOML 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
|
|
||||||
)
|
|
||||||
|
|
||||||
var quotedReplacer = strings.NewReplacer(
|
|
||||||
"\t", "\\t",
|
|
||||||
"\n", "\\n",
|
|
||||||
"\r", "\\r",
|
|
||||||
"\"", "\\\"",
|
|
||||||
"\\", "\\\\",
|
|
||||||
)
|
|
||||||
|
|
||||||
// Encoder controls the encoding of Go values to a TOML 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 TOML 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 TOML representation of the Go value to the underlying
|
|
||||||
// io.Writer. If the value given cannot be encoded to a valid TOML document,
|
|
||||||
// then an error is returned.
|
|
||||||
//
|
|
||||||
// The mapping between Go values and TOML 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
|
|
||||||
// TOML does not have any binary types.)
|
|
||||||
//
|
|
||||||
// When encoding TOML 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 TOML 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.(tomlEncodeError); 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()
|
|
||||||
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(tomlArrayHash, tomlTypeOfGo(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()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// By the TOML spec, 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()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
for i := 0; i < rv.Len(); i++ {
|
|
||||||
trv := rv.Index(i)
|
|
||||||
if isNil(trv) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
panicIfInvalidKey(key)
|
|
||||||
enc.newline()
|
|
||||||
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
|
|
||||||
enc.newline()
|
|
||||||
enc.eMapOrStruct(key, trv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
|
||||||
panicIfInvalidKey(key)
|
|
||||||
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 {
|
|
||||||
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll())
|
|
||||||
enc.newline()
|
|
||||||
}
|
|
||||||
enc.eMapOrStruct(key, rv)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) {
|
|
||||||
mapKeysSub = append(mapKeysSub, k)
|
|
||||||
} else {
|
|
||||||
mapKeysDirect = append(mapKeysDirect, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var writeMapKeys = func(mapKeys []string) {
|
|
||||||
sort.Strings(mapKeys)
|
|
||||||
for _, mapKey := range mapKeys {
|
|
||||||
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 unexported fields
|
|
||||||
if f.PkgPath != "" && !f.Anonymous {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
frv := rv.Field(i)
|
|
||||||
if f.Anonymous {
|
|
||||||
t := f.Type
|
|
||||||
switch t.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
addFields(t, frv, f.Index)
|
|
||||||
continue
|
|
||||||
case reflect.Ptr:
|
|
||||||
if t.Elem().Kind() == reflect.Struct {
|
|
||||||
if !frv.IsNil() {
|
|
||||||
addFields(t.Elem(), frv.Elem(), f.Index)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Fall through to the normal field encoding logic below
|
|
||||||
// for non-struct anonymous fields.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if typeIsHash(tomlTypeOfGo(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
|
|
||||||
}
|
|
||||||
|
|
||||||
tag := sft.Tag.Get("toml")
|
|
||||||
if tag == "-" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
keyName, opts := getOptions(tag)
|
|
||||||
if keyName == "" {
|
|
||||||
keyName = sft.Name
|
|
||||||
}
|
|
||||||
if _, ok := opts["omitempty"]; ok && isEmpty(sf) {
|
|
||||||
continue
|
|
||||||
} else if _, ok := opts["omitzero"]; ok && isZero(sf) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
enc.encode(key.add(keyName), sf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writeFields(fieldsDirect)
|
|
||||||
writeFields(fieldsSub)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tomlTypeName returns the TOML 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 TOML type of a Go value. The type may be `nil`, which means
|
|
||||||
// no concrete TOML type could be found.
|
|
||||||
func tomlTypeOfGo(rv reflect.Value) tomlType {
|
|
||||||
if isNil(rv) || !rv.IsValid() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Bool:
|
|
||||||
return tomlBool
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
|
||||||
reflect.Int64,
|
|
||||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
|
||||||
reflect.Uint64:
|
|
||||||
return tomlInteger
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
return tomlFloat
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
if typeEqual(tomlHash, tomlArrayType(rv)) {
|
|
||||||
return tomlArrayHash
|
|
||||||
} else {
|
|
||||||
return tomlArray
|
|
||||||
}
|
|
||||||
case reflect.Ptr, reflect.Interface:
|
|
||||||
return tomlTypeOfGo(rv.Elem())
|
|
||||||
case reflect.String:
|
|
||||||
return tomlString
|
|
||||||
case reflect.Map:
|
|
||||||
return tomlHash
|
|
||||||
case reflect.Struct:
|
|
||||||
switch rv.Interface().(type) {
|
|
||||||
case time.Time:
|
|
||||||
return tomlDatetime
|
|
||||||
case TextMarshaler:
|
|
||||||
return tomlString
|
|
||||||
default:
|
|
||||||
return tomlHash
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic("unexpected reflect.Kind: " + rv.Kind().String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tomlArrayType returns the element type of a TOML 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 TOML (such as nil elements, heterogeneous arrays or directly
|
|
||||||
// nested arrays of tables).
|
|
||||||
func tomlArrayType(rv reflect.Value) tomlType {
|
|
||||||
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
firstType := tomlTypeOfGo(rv.Index(0))
|
|
||||||
if firstType == nil {
|
|
||||||
encPanic(errArrayNilElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
rvlen := rv.Len()
|
|
||||||
for i := 1; i < rvlen; i++ {
|
|
||||||
elem := rv.Index(i)
|
|
||||||
switch elemType := tomlTypeOfGo(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, tomlArray) || typeEqual(firstType, tomlArrayHash) {
|
|
||||||
nest := tomlArrayType(eindirect(rv.Index(0)))
|
|
||||||
if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) {
|
|
||||||
encPanic(errArrayNoTable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return firstType
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOptions(keyName string) (string, map[string]struct{}) {
|
|
||||||
opts := make(map[string]struct{})
|
|
||||||
ss := strings.Split(keyName, ",")
|
|
||||||
name := ss[0]
|
|
||||||
if len(ss) > 1 {
|
|
||||||
for _, opt := range ss {
|
|
||||||
opts[opt] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return name, opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func isZero(rv reflect.Value) bool {
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
return rv.Int() == 0
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
||||||
return rv.Uint() == 0
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
return rv.Float() == 0.0
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEmpty(rv reflect.Value) bool {
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
|
||||||
return rv.Len() == 0
|
|
||||||
case reflect.Bool:
|
|
||||||
return !rv.Bool()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(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)-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func encPanic(err error) {
|
|
||||||
panic(tomlEncodeError{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) {
|
|
||||||
for _, k := range key {
|
|
||||||
if len(k) == 0 {
|
|
||||||
encPanic(e("Key '%s' is not a valid table name. Key names "+
|
|
||||||
"cannot be empty.", key.maybeQuotedAll()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidKeyName(s string) bool {
|
|
||||||
return len(s) != 0
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
// +build go1.2
|
|
||||||
|
|
||||||
package toml
|
|
||||||
|
|
||||||
// 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
|
|
|
@ -1,18 +0,0 @@
|
||||||
// +build !go1.2
|
|
||||||
|
|
||||||
package toml
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,871 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
type itemType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
itemError itemType = iota
|
|
||||||
itemNIL // used in the parser to indicate no type
|
|
||||||
itemEOF
|
|
||||||
itemText
|
|
||||||
itemString
|
|
||||||
itemRawString
|
|
||||||
itemMultilineString
|
|
||||||
itemRawMultilineString
|
|
||||||
itemBool
|
|
||||||
itemInteger
|
|
||||||
itemFloat
|
|
||||||
itemDatetime
|
|
||||||
itemArray // the start of an array
|
|
||||||
itemArrayEnd
|
|
||||||
itemTableStart
|
|
||||||
itemTableEnd
|
|
||||||
itemArrayTableStart
|
|
||||||
itemArrayTableEnd
|
|
||||||
itemKeyStart
|
|
||||||
itemCommentStart
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
eof = 0
|
|
||||||
tableStart = '['
|
|
||||||
tableEnd = ']'
|
|
||||||
arrayTableStart = '['
|
|
||||||
arrayTableEnd = ']'
|
|
||||||
tableSep = '.'
|
|
||||||
keySep = '='
|
|
||||||
arrayStart = '['
|
|
||||||
arrayEnd = ']'
|
|
||||||
arrayValTerm = ','
|
|
||||||
commentStart = '#'
|
|
||||||
stringStart = '"'
|
|
||||||
stringEnd = '"'
|
|
||||||
rawStringStart = '\''
|
|
||||||
rawStringEnd = '\''
|
|
||||||
)
|
|
||||||
|
|
||||||
type stateFn func(lx *lexer) stateFn
|
|
||||||
|
|
||||||
type lexer struct {
|
|
||||||
input string
|
|
||||||
start int
|
|
||||||
pos int
|
|
||||||
width int
|
|
||||||
line int
|
|
||||||
state stateFn
|
|
||||||
items chan item
|
|
||||||
|
|
||||||
// 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 + "\n",
|
|
||||||
state: lexTop,
|
|
||||||
line: 1,
|
|
||||||
items: make(chan item, 10),
|
|
||||||
stack: make([]stateFn, 0, 10),
|
|
||||||
}
|
|
||||||
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.")
|
|
||||||
}
|
|
||||||
last := lx.stack[len(lx.stack)-1]
|
|
||||||
lx.stack = lx.stack[0 : len(lx.stack)-1]
|
|
||||||
return last
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lx *lexer) current() string {
|
|
||||||
return lx.input[lx.start:lx.pos]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lx *lexer) emit(typ itemType) {
|
|
||||||
lx.items <- item{typ, lx.current(), lx.line}
|
|
||||||
lx.start = lx.pos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lx *lexer) emitTrim(typ itemType) {
|
|
||||||
lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line}
|
|
||||||
lx.start = lx.pos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lx *lexer) next() (r rune) {
|
|
||||||
if lx.pos >= len(lx.input) {
|
|
||||||
lx.width = 0
|
|
||||||
return eof
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
lx.pos -= lx.width
|
|
||||||
if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
|
|
||||||
lx.line--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// accept consumes the next rune if it's equal to `valid`.
|
|
||||||
func (lx *lexer) accept(valid rune) bool {
|
|
||||||
if lx.next() == valid {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
lx.backup()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
lx.items <- item{
|
|
||||||
itemError,
|
|
||||||
fmt.Sprintf(format, values...),
|
|
||||||
lx.line,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexTop consumes elements at the top level of TOML data.
|
|
||||||
func lexTop(lx *lexer) stateFn {
|
|
||||||
r := lx.next()
|
|
||||||
if isWhitespace(r) || isNL(r) {
|
|
||||||
return lexSkip(lx, lexTop)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r {
|
|
||||||
case commentStart:
|
|
||||||
lx.push(lexTop)
|
|
||||||
return lexCommentStart
|
|
||||||
case tableStart:
|
|
||||||
return lexTableStart
|
|
||||||
case 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(lexTopEnd)
|
|
||||||
return lexKeyStart
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexTopEnd is entered whenever a top-level item has been consumed. (A value
|
|
||||||
// or a table.) 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 lexTopEnd(lx *lexer) stateFn {
|
|
||||||
r := lx.next()
|
|
||||||
switch {
|
|
||||||
case r == commentStart:
|
|
||||||
// a comment will read to a new line for us.
|
|
||||||
lx.push(lexTop)
|
|
||||||
return lexCommentStart
|
|
||||||
case isWhitespace(r):
|
|
||||||
return lexTopEnd
|
|
||||||
case isNL(r):
|
|
||||||
lx.ignore()
|
|
||||||
return lexTop
|
|
||||||
case r == eof:
|
|
||||||
lx.ignore()
|
|
||||||
return lexTop
|
|
||||||
}
|
|
||||||
return lx.errorf("Expected a top-level item to end with a new line, "+
|
|
||||||
"comment or EOF, but got %q instead.", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexTable lexes the beginning of a table. Namely, it makes sure that
|
|
||||||
// it starts with a character other than '.' and ']'.
|
|
||||||
// It assumes that '[' has already been consumed.
|
|
||||||
// It also handles the case that this is an item in an array of tables.
|
|
||||||
// e.g., '[[name]]'.
|
|
||||||
func lexTableStart(lx *lexer) stateFn {
|
|
||||||
if lx.peek() == arrayTableStart {
|
|
||||||
lx.next()
|
|
||||||
lx.emit(itemArrayTableStart)
|
|
||||||
lx.push(lexArrayTableEnd)
|
|
||||||
} else {
|
|
||||||
lx.emit(itemTableStart)
|
|
||||||
lx.push(lexTableEnd)
|
|
||||||
}
|
|
||||||
return lexTableNameStart
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexTableEnd(lx *lexer) stateFn {
|
|
||||||
lx.emit(itemTableEnd)
|
|
||||||
return lexTopEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexArrayTableEnd(lx *lexer) stateFn {
|
|
||||||
if r := lx.next(); r != arrayTableEnd {
|
|
||||||
return lx.errorf("Expected end of table array name delimiter %q, "+
|
|
||||||
"but got %q instead.", arrayTableEnd, r)
|
|
||||||
}
|
|
||||||
lx.emit(itemArrayTableEnd)
|
|
||||||
return lexTopEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexTableNameStart(lx *lexer) stateFn {
|
|
||||||
switch r := lx.peek(); {
|
|
||||||
case r == tableEnd || r == eof:
|
|
||||||
return lx.errorf("Unexpected end of table name. (Table names cannot " +
|
|
||||||
"be empty.)")
|
|
||||||
case r == tableSep:
|
|
||||||
return lx.errorf("Unexpected table separator. (Table names cannot " +
|
|
||||||
"be empty.)")
|
|
||||||
case r == stringStart || r == rawStringStart:
|
|
||||||
lx.ignore()
|
|
||||||
lx.push(lexTableNameEnd)
|
|
||||||
return lexValue // reuse string lexing
|
|
||||||
default:
|
|
||||||
return lexBareTableName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexTableName lexes the name of a table. It assumes that at least one
|
|
||||||
// valid character for the table has already been read.
|
|
||||||
func lexBareTableName(lx *lexer) stateFn {
|
|
||||||
switch r := lx.next(); {
|
|
||||||
case isBareKeyChar(r):
|
|
||||||
return lexBareTableName
|
|
||||||
case r == tableSep || r == tableEnd:
|
|
||||||
lx.backup()
|
|
||||||
lx.emitTrim(itemText)
|
|
||||||
return lexTableNameEnd
|
|
||||||
default:
|
|
||||||
return lx.errorf("Bare keys cannot contain %q.", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexTableNameEnd reads the end of a piece of a table name, optionally
|
|
||||||
// consuming whitespace.
|
|
||||||
func lexTableNameEnd(lx *lexer) stateFn {
|
|
||||||
switch r := lx.next(); {
|
|
||||||
case isWhitespace(r):
|
|
||||||
return lexTableNameEnd
|
|
||||||
case r == tableSep:
|
|
||||||
lx.ignore()
|
|
||||||
return lexTableNameStart
|
|
||||||
case r == tableEnd:
|
|
||||||
return lx.pop()
|
|
||||||
default:
|
|
||||||
return lx.errorf("Expected '.' or ']' to end table name, but got %q "+
|
|
||||||
"instead.", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexKeyStart consumes a key name up until the first non-whitespace character.
|
|
||||||
// lexKeyStart will ignore whitespace.
|
|
||||||
func lexKeyStart(lx *lexer) stateFn {
|
|
||||||
r := lx.peek()
|
|
||||||
switch {
|
|
||||||
case r == keySep:
|
|
||||||
return lx.errorf("Unexpected key separator %q.", keySep)
|
|
||||||
case isWhitespace(r) || isNL(r):
|
|
||||||
lx.next()
|
|
||||||
return lexSkip(lx, lexKeyStart)
|
|
||||||
case r == stringStart || r == rawStringStart:
|
|
||||||
lx.ignore()
|
|
||||||
lx.emit(itemKeyStart)
|
|
||||||
lx.push(lexKeyEnd)
|
|
||||||
return lexValue // reuse string lexing
|
|
||||||
default:
|
|
||||||
lx.ignore()
|
|
||||||
lx.emit(itemKeyStart)
|
|
||||||
return lexBareKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexBareKey consumes the text of a bare key. Assumes that the first character
|
|
||||||
// (which is not whitespace) has not yet been consumed.
|
|
||||||
func lexBareKey(lx *lexer) stateFn {
|
|
||||||
switch r := lx.next(); {
|
|
||||||
case isBareKeyChar(r):
|
|
||||||
return lexBareKey
|
|
||||||
case isWhitespace(r):
|
|
||||||
lx.emitTrim(itemText)
|
|
||||||
return lexKeyEnd
|
|
||||||
case r == keySep:
|
|
||||||
lx.backup()
|
|
||||||
lx.emitTrim(itemText)
|
|
||||||
return lexKeyEnd
|
|
||||||
default:
|
|
||||||
return lx.errorf("Bare keys cannot contain %q.", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexKeyEnd consumes the end of a key and trims whitespace (up to the key
|
|
||||||
// separator).
|
|
||||||
func lexKeyEnd(lx *lexer) stateFn {
|
|
||||||
switch r := lx.next(); {
|
|
||||||
case r == keySep:
|
|
||||||
return lexSkip(lx, lexValue)
|
|
||||||
case isWhitespace(r):
|
|
||||||
return lexSkip(lx, lexKeyEnd)
|
|
||||||
default:
|
|
||||||
return lx.errorf("Expected key separator %q, but got %q instead.",
|
|
||||||
keySep, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
if isWhitespace(r) {
|
|
||||||
return lexSkip(lx, lexValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case r == arrayStart:
|
|
||||||
lx.ignore()
|
|
||||||
lx.emit(itemArray)
|
|
||||||
return lexArrayValue
|
|
||||||
case r == stringStart:
|
|
||||||
if lx.accept(stringStart) {
|
|
||||||
if lx.accept(stringStart) {
|
|
||||||
lx.ignore() // Ignore """
|
|
||||||
return lexMultilineString
|
|
||||||
}
|
|
||||||
lx.backup()
|
|
||||||
}
|
|
||||||
lx.ignore() // ignore the '"'
|
|
||||||
return lexString
|
|
||||||
case r == rawStringStart:
|
|
||||||
if lx.accept(rawStringStart) {
|
|
||||||
if lx.accept(rawStringStart) {
|
|
||||||
lx.ignore() // Ignore """
|
|
||||||
return lexMultilineRawString
|
|
||||||
}
|
|
||||||
lx.backup()
|
|
||||||
}
|
|
||||||
lx.ignore() // ignore the "'"
|
|
||||||
return lexRawString
|
|
||||||
case r == 't':
|
|
||||||
return lexTrue
|
|
||||||
case r == 'f':
|
|
||||||
return lexFalse
|
|
||||||
case r == '-':
|
|
||||||
return lexNumberStart
|
|
||||||
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 '.'.")
|
|
||||||
}
|
|
||||||
return lx.errorf("Expected value but found %q 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()
|
|
||||||
switch {
|
|
||||||
case isWhitespace(r) || isNL(r):
|
|
||||||
return lexSkip(lx, lexArrayValue)
|
|
||||||
case r == commentStart:
|
|
||||||
lx.push(lexArrayValue)
|
|
||||||
return lexCommentStart
|
|
||||||
case r == arrayValTerm:
|
|
||||||
return lx.errorf("Unexpected array value terminator %q.",
|
|
||||||
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()
|
|
||||||
switch {
|
|
||||||
case isWhitespace(r) || isNL(r):
|
|
||||||
return lexSkip(lx, lexArrayValueEnd)
|
|
||||||
case r == commentStart:
|
|
||||||
lx.push(lexArrayValueEnd)
|
|
||||||
return lexCommentStart
|
|
||||||
case r == arrayValTerm:
|
|
||||||
lx.ignore()
|
|
||||||
return lexArrayValue // move on to the next value
|
|
||||||
case r == arrayEnd:
|
|
||||||
return lexArrayEnd
|
|
||||||
}
|
|
||||||
return lx.errorf("Expected an array value terminator %q or an array "+
|
|
||||||
"terminator %q, but got %q 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)
|
|
||||||
return lx.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
switch {
|
|
||||||
case isNL(r):
|
|
||||||
return lx.errorf("Strings cannot contain new lines.")
|
|
||||||
case r == '\\':
|
|
||||||
lx.push(lexString)
|
|
||||||
return lexStringEscape
|
|
||||||
case r == stringEnd:
|
|
||||||
lx.backup()
|
|
||||||
lx.emit(itemString)
|
|
||||||
lx.next()
|
|
||||||
lx.ignore()
|
|
||||||
return lx.pop()
|
|
||||||
}
|
|
||||||
return lexString
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexMultilineString consumes the inner contents of a string. It assumes that
|
|
||||||
// the beginning '"""' has already been consumed and ignored.
|
|
||||||
func lexMultilineString(lx *lexer) stateFn {
|
|
||||||
r := lx.next()
|
|
||||||
switch {
|
|
||||||
case r == '\\':
|
|
||||||
return lexMultilineStringEscape
|
|
||||||
case r == stringEnd:
|
|
||||||
if lx.accept(stringEnd) {
|
|
||||||
if lx.accept(stringEnd) {
|
|
||||||
lx.backup()
|
|
||||||
lx.backup()
|
|
||||||
lx.backup()
|
|
||||||
lx.emit(itemMultilineString)
|
|
||||||
lx.next()
|
|
||||||
lx.next()
|
|
||||||
lx.next()
|
|
||||||
lx.ignore()
|
|
||||||
return lx.pop()
|
|
||||||
}
|
|
||||||
lx.backup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lexMultilineString
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexRawString consumes a raw string. Nothing can be escaped in such a string.
|
|
||||||
// It assumes that the beginning "'" has already been consumed and ignored.
|
|
||||||
func lexRawString(lx *lexer) stateFn {
|
|
||||||
r := lx.next()
|
|
||||||
switch {
|
|
||||||
case isNL(r):
|
|
||||||
return lx.errorf("Strings cannot contain new lines.")
|
|
||||||
case r == rawStringEnd:
|
|
||||||
lx.backup()
|
|
||||||
lx.emit(itemRawString)
|
|
||||||
lx.next()
|
|
||||||
lx.ignore()
|
|
||||||
return lx.pop()
|
|
||||||
}
|
|
||||||
return lexRawString
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexMultilineRawString consumes a raw string. Nothing can be escaped in such
|
|
||||||
// a string. It assumes that the beginning "'" has already been consumed and
|
|
||||||
// ignored.
|
|
||||||
func lexMultilineRawString(lx *lexer) stateFn {
|
|
||||||
r := lx.next()
|
|
||||||
switch {
|
|
||||||
case r == rawStringEnd:
|
|
||||||
if lx.accept(rawStringEnd) {
|
|
||||||
if lx.accept(rawStringEnd) {
|
|
||||||
lx.backup()
|
|
||||||
lx.backup()
|
|
||||||
lx.backup()
|
|
||||||
lx.emit(itemRawMultilineString)
|
|
||||||
lx.next()
|
|
||||||
lx.next()
|
|
||||||
lx.next()
|
|
||||||
lx.ignore()
|
|
||||||
return lx.pop()
|
|
||||||
}
|
|
||||||
lx.backup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lexMultilineRawString
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexMultilineStringEscape consumes an escaped character. It assumes that the
|
|
||||||
// preceding '\\' has already been consumed.
|
|
||||||
func lexMultilineStringEscape(lx *lexer) stateFn {
|
|
||||||
// Handle the special case first:
|
|
||||||
if isNL(lx.next()) {
|
|
||||||
return lexMultilineString
|
|
||||||
} else {
|
|
||||||
lx.backup()
|
|
||||||
lx.push(lexMultilineString)
|
|
||||||
return lexStringEscape(lx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexStringEscape(lx *lexer) stateFn {
|
|
||||||
r := lx.next()
|
|
||||||
switch r {
|
|
||||||
case 'b':
|
|
||||||
fallthrough
|
|
||||||
case 't':
|
|
||||||
fallthrough
|
|
||||||
case 'n':
|
|
||||||
fallthrough
|
|
||||||
case 'f':
|
|
||||||
fallthrough
|
|
||||||
case 'r':
|
|
||||||
fallthrough
|
|
||||||
case '"':
|
|
||||||
fallthrough
|
|
||||||
case '\\':
|
|
||||||
return lx.pop()
|
|
||||||
case 'u':
|
|
||||||
return lexShortUnicodeEscape
|
|
||||||
case 'U':
|
|
||||||
return lexLongUnicodeEscape
|
|
||||||
}
|
|
||||||
return lx.errorf("Invalid escape character %q. Only the following "+
|
|
||||||
"escape characters are allowed: "+
|
|
||||||
"\\b, \\t, \\n, \\f, \\r, \\\", \\/, \\\\, "+
|
|
||||||
"\\uXXXX and \\UXXXXXXXX.", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexShortUnicodeEscape(lx *lexer) stateFn {
|
|
||||||
var r rune
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
r = lx.next()
|
|
||||||
if !isHexadecimal(r) {
|
|
||||||
return lx.errorf("Expected four hexadecimal digits after '\\u', "+
|
|
||||||
"but got '%s' instead.", lx.current())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lx.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexLongUnicodeEscape(lx *lexer) stateFn {
|
|
||||||
var r rune
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
r = lx.next()
|
|
||||||
if !isHexadecimal(r) {
|
|
||||||
return lx.errorf("Expected eight hexadecimal digits after '\\U', "+
|
|
||||||
"but got '%s' instead.", lx.current())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lx.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 %q.", 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 %q instead.", r)
|
|
||||||
}
|
|
||||||
} else if f != r {
|
|
||||||
return lx.errorf("Expected %q in ISO8601 datetime, "+
|
|
||||||
"but found %q 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 %q.", 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 "+
|
|
||||||
"%q 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexConst consumes the s[1:] in s. It assumes that s[0] has already been
|
|
||||||
// consumed.
|
|
||||||
func lexConst(lx *lexer, s string) stateFn {
|
|
||||||
for i := range s[1:] {
|
|
||||||
if r := lx.next(); r != rune(s[i+1]) {
|
|
||||||
return lx.errorf("Expected %q, but found %q instead.", s[:i+1],
|
|
||||||
s[:i]+string(r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexTrue consumes the "rue" in "true". It assumes that 't' has already
|
|
||||||
// been consumed.
|
|
||||||
func lexTrue(lx *lexer) stateFn {
|
|
||||||
if fn := lexConst(lx, "true"); fn != nil {
|
|
||||||
return fn
|
|
||||||
}
|
|
||||||
lx.emit(itemBool)
|
|
||||||
return lx.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexFalse consumes the "alse" in "false". It assumes that 'f' has already
|
|
||||||
// been consumed.
|
|
||||||
func lexFalse(lx *lexer) stateFn {
|
|
||||||
if fn := lexConst(lx, "false"); fn != nil {
|
|
||||||
return fn
|
|
||||||
}
|
|
||||||
lx.emit(itemBool)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 isBareKeyChar(r rune) bool {
|
|
||||||
return (r >= 'A' && r <= 'Z') ||
|
|
||||||
(r >= 'a' && r <= 'z') ||
|
|
||||||
(r >= '0' && r <= '9') ||
|
|
||||||
r == '_' ||
|
|
||||||
r == '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
func (itype itemType) String() string {
|
|
||||||
switch itype {
|
|
||||||
case itemError:
|
|
||||||
return "Error"
|
|
||||||
case itemNIL:
|
|
||||||
return "NIL"
|
|
||||||
case itemEOF:
|
|
||||||
return "EOF"
|
|
||||||
case itemText:
|
|
||||||
return "Text"
|
|
||||||
case itemString:
|
|
||||||
return "String"
|
|
||||||
case itemRawString:
|
|
||||||
return "String"
|
|
||||||
case itemMultilineString:
|
|
||||||
return "String"
|
|
||||||
case itemRawMultilineString:
|
|
||||||
return "String"
|
|
||||||
case itemBool:
|
|
||||||
return "Bool"
|
|
||||||
case itemInteger:
|
|
||||||
return "Integer"
|
|
||||||
case itemFloat:
|
|
||||||
return "Float"
|
|
||||||
case itemDatetime:
|
|
||||||
return "DateTime"
|
|
||||||
case itemTableStart:
|
|
||||||
return "TableStart"
|
|
||||||
case itemTableEnd:
|
|
||||||
return "TableEnd"
|
|
||||||
case itemKeyStart:
|
|
||||||
return "KeyStart"
|
|
||||||
case itemArray:
|
|
||||||
return "Array"
|
|
||||||
case itemArrayEnd:
|
|
||||||
return "ArrayEnd"
|
|
||||||
case itemCommentStart:
|
|
||||||
return "CommentStart"
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (item item) String() string {
|
|
||||||
return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val)
|
|
||||||
}
|
|
|
@ -1,493 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
type parser struct {
|
|
||||||
mapping map[string]interface{}
|
|
||||||
types map[string]tomlType
|
|
||||||
lx *lexer
|
|
||||||
|
|
||||||
// A list of keys in the order that they appear in the TOML 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
|
|
||||||
|
|
||||||
// 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) (p *parser, err error) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
var ok bool
|
|
||||||
if err, ok = r.(parseError); ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
panic(r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
p = &parser{
|
|
||||||
mapping: make(map[string]interface{}),
|
|
||||||
types: make(map[string]tomlType),
|
|
||||||
lx: lex(data),
|
|
||||||
ordered: make([]Key, 0),
|
|
||||||
implicits: make(map[string]bool),
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
item := p.next()
|
|
||||||
if item.typ == itemEOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
p.topLevel(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) panicf(format string, v ...interface{}) {
|
|
||||||
msg := fmt.Sprintf("Near line %d (last key parsed '%s'): %s",
|
|
||||||
p.approxLine, p.current(), fmt.Sprintf(format, v...))
|
|
||||||
panic(parseError(msg))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) next() item {
|
|
||||||
it := p.lx.nextItem()
|
|
||||||
if it.typ == itemError {
|
|
||||||
p.panicf("%s", it.val)
|
|
||||||
}
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) bug(format string, v ...interface{}) {
|
|
||||||
log.Panicf("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) topLevel(item item) {
|
|
||||||
switch item.typ {
|
|
||||||
case itemCommentStart:
|
|
||||||
p.approxLine = item.line
|
|
||||||
p.expect(itemText)
|
|
||||||
case itemTableStart:
|
|
||||||
kg := p.next()
|
|
||||||
p.approxLine = kg.line
|
|
||||||
|
|
||||||
var key Key
|
|
||||||
for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() {
|
|
||||||
key = append(key, p.keyString(kg))
|
|
||||||
}
|
|
||||||
p.assertEqual(itemTableEnd, kg.typ)
|
|
||||||
|
|
||||||
p.establishContext(key, false)
|
|
||||||
p.setType("", tomlHash)
|
|
||||||
p.ordered = append(p.ordered, key)
|
|
||||||
case itemArrayTableStart:
|
|
||||||
kg := p.next()
|
|
||||||
p.approxLine = kg.line
|
|
||||||
|
|
||||||
var key Key
|
|
||||||
for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() {
|
|
||||||
key = append(key, p.keyString(kg))
|
|
||||||
}
|
|
||||||
p.assertEqual(itemArrayTableEnd, kg.typ)
|
|
||||||
|
|
||||||
p.establishContext(key, true)
|
|
||||||
p.setType("", tomlArrayHash)
|
|
||||||
p.ordered = append(p.ordered, key)
|
|
||||||
case itemKeyStart:
|
|
||||||
kname := p.next()
|
|
||||||
p.approxLine = kname.line
|
|
||||||
p.currentKey = p.keyString(kname)
|
|
||||||
|
|
||||||
val, typ := p.value(p.next())
|
|
||||||
p.setValue(p.currentKey, val)
|
|
||||||
p.setType(p.currentKey, typ)
|
|
||||||
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
|
||||||
p.currentKey = ""
|
|
||||||
default:
|
|
||||||
p.bug("Unexpected type at top level: %s", item.typ)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets a string for a key (or part of a key in a table name).
|
|
||||||
func (p *parser) keyString(it item) string {
|
|
||||||
switch it.typ {
|
|
||||||
case itemText:
|
|
||||||
return it.val
|
|
||||||
case itemString, itemMultilineString,
|
|
||||||
itemRawString, itemRawMultilineString:
|
|
||||||
s, _ := p.value(it)
|
|
||||||
return s.(string)
|
|
||||||
default:
|
|
||||||
p.bug("Unexpected key type: %s", it.typ)
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// value translates an expected value from the lexer into a Go value wrapped
|
|
||||||
// as an empty interface.
|
|
||||||
func (p *parser) value(it item) (interface{}, tomlType) {
|
|
||||||
switch it.typ {
|
|
||||||
case itemString:
|
|
||||||
return p.replaceEscapes(it.val), p.typeOfPrimitive(it)
|
|
||||||
case itemMultilineString:
|
|
||||||
trimmed := stripFirstNewline(stripEscapedWhitespace(it.val))
|
|
||||||
return p.replaceEscapes(trimmed), p.typeOfPrimitive(it)
|
|
||||||
case itemRawString:
|
|
||||||
return it.val, p.typeOfPrimitive(it)
|
|
||||||
case itemRawMultilineString:
|
|
||||||
return stripFirstNewline(it.val), p.typeOfPrimitive(it)
|
|
||||||
case itemBool:
|
|
||||||
switch it.val {
|
|
||||||
case "true":
|
|
||||||
return true, p.typeOfPrimitive(it)
|
|
||||||
case "false":
|
|
||||||
return false, p.typeOfPrimitive(it)
|
|
||||||
}
|
|
||||||
p.bug("Expected boolean value, but got '%s'.", it.val)
|
|
||||||
case itemInteger:
|
|
||||||
num, err := strconv.ParseInt(it.val, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
// See comment below for floats describing why we make a
|
|
||||||
// distinction between a bug and a user error.
|
|
||||||
if e, ok := err.(*strconv.NumError); ok &&
|
|
||||||
e.Err == strconv.ErrRange {
|
|
||||||
|
|
||||||
p.panicf("Integer '%s' is out of the range of 64-bit "+
|
|
||||||
"signed integers.", it.val)
|
|
||||||
} else {
|
|
||||||
p.bug("Expected integer value, but got '%s'.", it.val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return num, p.typeOfPrimitive(it)
|
|
||||||
case itemFloat:
|
|
||||||
num, err := strconv.ParseFloat(it.val, 64)
|
|
||||||
if err != nil {
|
|
||||||
// Distinguish float values. Normally, it'd be a bug if the lexer
|
|
||||||
// provides an invalid float, but it's possible that the float is
|
|
||||||
// out of range of valid values (which the lexer cannot determine).
|
|
||||||
// So mark the former as a bug but the latter as a legitimate user
|
|
||||||
// error.
|
|
||||||
//
|
|
||||||
// This is also true for integers.
|
|
||||||
if e, ok := err.(*strconv.NumError); ok &&
|
|
||||||
e.Err == strconv.ErrRange {
|
|
||||||
|
|
||||||
p.panicf("Float '%s' is out of the range of 64-bit "+
|
|
||||||
"IEEE-754 floating-point numbers.", it.val)
|
|
||||||
} else {
|
|
||||||
p.bug("Expected float value, but got '%s'.", it.val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return num, p.typeOfPrimitive(it)
|
|
||||||
case itemDatetime:
|
|
||||||
t, err := time.Parse("2006-01-02T15:04:05Z", it.val)
|
|
||||||
if err != nil {
|
|
||||||
p.panicf("Invalid RFC3339 Zulu DateTime: '%s'.", it.val)
|
|
||||||
}
|
|
||||||
return t, p.typeOfPrimitive(it)
|
|
||||||
case itemArray:
|
|
||||||
array := make([]interface{}, 0)
|
|
||||||
types := make([]tomlType, 0)
|
|
||||||
|
|
||||||
for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
|
|
||||||
if it.typ == itemCommentStart {
|
|
||||||
p.expect(itemText)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val, typ := p.value(it)
|
|
||||||
array = append(array, val)
|
|
||||||
types = append(types, typ)
|
|
||||||
}
|
|
||||||
return array, p.typeOfArray(types)
|
|
||||||
}
|
|
||||||
p.bug("Unexpected value type: %s", it.typ)
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
|
|
||||||
// establishContext sets the current context of the parser,
|
|
||||||
// where the context is either a hash or an array of hashes. Which one is
|
|
||||||
// set depends on the value of the `array` parameter.
|
|
||||||
//
|
|
||||||
// Establishing the context also makes sure that the key isn't a duplicate, and
|
|
||||||
// will create implicit hashes automatically.
|
|
||||||
func (p *parser) establishContext(key Key, array bool) {
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
// Always start at the top level and drill down for our context.
|
|
||||||
hashContext := p.mapping
|
|
||||||
keyContext := make(Key, 0)
|
|
||||||
|
|
||||||
// We only need implicit hashes for key[0:-1]
|
|
||||||
for _, k := range key[0 : len(key)-1] {
|
|
||||||
_, ok = hashContext[k]
|
|
||||||
keyContext = append(keyContext, k)
|
|
||||||
|
|
||||||
// No key? Make an implicit hash and move on.
|
|
||||||
if !ok {
|
|
||||||
p.addImplicit(keyContext)
|
|
||||||
hashContext[k] = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the hash context is actually an array of tables, then set
|
|
||||||
// the hash context to the last element in that array.
|
|
||||||
//
|
|
||||||
// Otherwise, it better be a table, since this MUST be a key group (by
|
|
||||||
// virtue of it not being the last element in a key).
|
|
||||||
switch t := hashContext[k].(type) {
|
|
||||||
case []map[string]interface{}:
|
|
||||||
hashContext = t[len(t)-1]
|
|
||||||
case map[string]interface{}:
|
|
||||||
hashContext = t
|
|
||||||
default:
|
|
||||||
p.panicf("Key '%s' was already created as a hash.", keyContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.context = keyContext
|
|
||||||
if array {
|
|
||||||
// If this is the first element for this array, then allocate a new
|
|
||||||
// list of tables for it.
|
|
||||||
k := key[len(key)-1]
|
|
||||||
if _, ok := hashContext[k]; !ok {
|
|
||||||
hashContext[k] = make([]map[string]interface{}, 0, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new table. But make sure the key hasn't already been used
|
|
||||||
// for something else.
|
|
||||||
if hash, ok := hashContext[k].([]map[string]interface{}); ok {
|
|
||||||
hashContext[k] = append(hash, make(map[string]interface{}))
|
|
||||||
} else {
|
|
||||||
p.panicf("Key '%s' was already created and cannot be used as "+
|
|
||||||
"an array.", keyContext)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.setValue(key[len(key)-1], make(map[string]interface{}))
|
|
||||||
}
|
|
||||||
p.context = append(p.context, key[len(key)-1])
|
|
||||||
}
|
|
||||||
|
|
||||||
// setValue sets the given key to the given value in the current context.
|
|
||||||
// It will make sure that the key hasn't already been defined, account for
|
|
||||||
// implicit key groups.
|
|
||||||
func (p *parser) setValue(key string, value interface{}) {
|
|
||||||
var tmpHash interface{}
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
hash := p.mapping
|
|
||||||
keyContext := make(Key, 0)
|
|
||||||
for _, k := range p.context {
|
|
||||||
keyContext = append(keyContext, k)
|
|
||||||
if tmpHash, ok = hash[k]; !ok {
|
|
||||||
p.bug("Context for key '%s' has not been established.", keyContext)
|
|
||||||
}
|
|
||||||
switch t := tmpHash.(type) {
|
|
||||||
case []map[string]interface{}:
|
|
||||||
// The context is a table of hashes. Pick the most recent table
|
|
||||||
// defined as the current hash.
|
|
||||||
hash = t[len(t)-1]
|
|
||||||
case map[string]interface{}:
|
|
||||||
hash = t
|
|
||||||
default:
|
|
||||||
p.bug("Expected hash to have type 'map[string]interface{}', but "+
|
|
||||||
"it has '%T' instead.", tmpHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
keyContext = append(keyContext, key)
|
|
||||||
|
|
||||||
if _, ok := hash[key]; ok {
|
|
||||||
// Typically, if the given key has already been set, then we have
|
|
||||||
// to raise an error since duplicate keys are disallowed. However,
|
|
||||||
// it's possible that a key was previously defined implicitly. In this
|
|
||||||
// case, it is allowed to be redefined concretely. (See the
|
|
||||||
// `tests/valid/implicit-and-explicit-after.toml` test in `toml-test`.)
|
|
||||||
//
|
|
||||||
// But we have to make sure to stop marking it as an implicit. (So that
|
|
||||||
// another redefinition provokes an error.)
|
|
||||||
//
|
|
||||||
// Note that since it has already been defined (as a hash), we don't
|
|
||||||
// want to overwrite it. So our business is done.
|
|
||||||
if p.isImplicit(keyContext) {
|
|
||||||
p.removeImplicit(keyContext)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, we have a concrete key trying to override a previous
|
|
||||||
// key, which is *always* wrong.
|
|
||||||
p.panicf("Key '%s' has already been defined.", keyContext)
|
|
||||||
}
|
|
||||||
hash[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 tomlType) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripFirstNewline(s string) string {
|
|
||||||
if len(s) == 0 || s[0] != '\n' {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return s[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripEscapedWhitespace(s string) string {
|
|
||||||
esc := strings.Split(s, "\\\n")
|
|
||||||
if len(esc) > 1 {
|
|
||||||
for i := 1; i < len(esc); i++ {
|
|
||||||
esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(esc, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) replaceEscapes(str string) string {
|
|
||||||
var replaced []rune
|
|
||||||
s := []byte(str)
|
|
||||||
r := 0
|
|
||||||
for r < len(s) {
|
|
||||||
if s[r] != '\\' {
|
|
||||||
c, size := utf8.DecodeRune(s[r:])
|
|
||||||
r += size
|
|
||||||
replaced = append(replaced, c)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r += 1
|
|
||||||
if r >= len(s) {
|
|
||||||
p.bug("Escape sequence at end of string.")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
switch s[r] {
|
|
||||||
default:
|
|
||||||
p.bug("Expected valid escape code after \\, but got %q.", s[r])
|
|
||||||
return ""
|
|
||||||
case 'b':
|
|
||||||
replaced = append(replaced, rune(0x0008))
|
|
||||||
r += 1
|
|
||||||
case 't':
|
|
||||||
replaced = append(replaced, rune(0x0009))
|
|
||||||
r += 1
|
|
||||||
case 'n':
|
|
||||||
replaced = append(replaced, rune(0x000A))
|
|
||||||
r += 1
|
|
||||||
case 'f':
|
|
||||||
replaced = append(replaced, rune(0x000C))
|
|
||||||
r += 1
|
|
||||||
case 'r':
|
|
||||||
replaced = append(replaced, rune(0x000D))
|
|
||||||
r += 1
|
|
||||||
case '"':
|
|
||||||
replaced = append(replaced, rune(0x0022))
|
|
||||||
r += 1
|
|
||||||
case '\\':
|
|
||||||
replaced = append(replaced, rune(0x005C))
|
|
||||||
r += 1
|
|
||||||
case 'u':
|
|
||||||
// At this point, we know we have a Unicode escape of the form
|
|
||||||
// `uXXXX` at [r, r+5). (Because the lexer guarantees this
|
|
||||||
// for us.)
|
|
||||||
escaped := p.asciiEscapeToUnicode(s[r+1 : r+5])
|
|
||||||
replaced = append(replaced, escaped)
|
|
||||||
r += 5
|
|
||||||
case 'U':
|
|
||||||
// At this point, we know we have a Unicode escape of the form
|
|
||||||
// `uXXXX` at [r, r+9). (Because the lexer guarantees this
|
|
||||||
// for us.)
|
|
||||||
escaped := p.asciiEscapeToUnicode(s[r+1 : r+9])
|
|
||||||
replaced = append(replaced, escaped)
|
|
||||||
r += 9
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return string(replaced)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) asciiEscapeToUnicode(bs []byte) rune {
|
|
||||||
s := string(bs)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if !utf8.ValidRune(rune(hex)) {
|
|
||||||
p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s)
|
|
||||||
}
|
|
||||||
return rune(hex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isStringType(ty itemType) bool {
|
|
||||||
return ty == itemString || ty == itemMultilineString ||
|
|
||||||
ty == itemRawString || ty == itemRawMultilineString
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
// tomlType represents any Go type that corresponds to a TOML type.
|
|
||||||
// While the first draft of the TOML spec has a simplistic type system that
|
|
||||||
// probably doesn't need this level of sophistication, we seem to be militating
|
|
||||||
// toward adding real composite types.
|
|
||||||
type tomlType interface {
|
|
||||||
typeString() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// typeEqual accepts any two types and returns true if they are equal.
|
|
||||||
func typeEqual(t1, t2 tomlType) bool {
|
|
||||||
if t1 == nil || t2 == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return t1.typeString() == t2.typeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
func typeIsHash(t tomlType) bool {
|
|
||||||
return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
type tomlBaseType string
|
|
||||||
|
|
||||||
func (btype tomlBaseType) typeString() string {
|
|
||||||
return string(btype)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (btype tomlBaseType) String() string {
|
|
||||||
return btype.typeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
tomlInteger tomlBaseType = "Integer"
|
|
||||||
tomlFloat tomlBaseType = "Float"
|
|
||||||
tomlDatetime tomlBaseType = "Datetime"
|
|
||||||
tomlString tomlBaseType = "String"
|
|
||||||
tomlBool tomlBaseType = "Bool"
|
|
||||||
tomlArray tomlBaseType = "Array"
|
|
||||||
tomlHash tomlBaseType = "Hash"
|
|
||||||
tomlArrayHash tomlBaseType = "ArrayHash"
|
|
||||||
)
|
|
||||||
|
|
||||||
// typeOfPrimitive returns a tomlType of any primitive value in TOML.
|
|
||||||
// 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) tomlType {
|
|
||||||
switch lexItem.typ {
|
|
||||||
case itemInteger:
|
|
||||||
return tomlInteger
|
|
||||||
case itemFloat:
|
|
||||||
return tomlFloat
|
|
||||||
case itemDatetime:
|
|
||||||
return tomlDatetime
|
|
||||||
case itemString:
|
|
||||||
return tomlString
|
|
||||||
case itemMultilineString:
|
|
||||||
return tomlString
|
|
||||||
case itemRawString:
|
|
||||||
return tomlString
|
|
||||||
case itemRawMultilineString:
|
|
||||||
return tomlString
|
|
||||||
case itemBool:
|
|
||||||
return tomlBool
|
|
||||||
}
|
|
||||||
p.bug("Cannot infer primitive type of lex item '%s'.", lexItem)
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
|
|
||||||
// typeOfArray returns a tomlType 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 []tomlType) tomlType {
|
|
||||||
// Empty arrays are cool.
|
|
||||||
if len(types) == 0 {
|
|
||||||
return tomlArray
|
|
||||||
}
|
|
||||||
|
|
||||||
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 tomlArray
|
|
||||||
}
|
|
|
@ -1,241 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A field represents a single field found in a struct.
|
|
||||||
type field struct {
|
|
||||||
name string // the name of the field (`toml` tag included)
|
|
||||||
tag bool // whether field has a `toml` 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 toml 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 TOML 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 != "" && !sf.Anonymous { // unexported
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name, _ := getOptions(sf.Tag.Get("toml"))
|
|
||||||
if name == "-" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
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 TOML 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
|
|
||||||
// TOML 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
|
|
||||||
}
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
// Package buffruneio is a wrapper around bufio to provide buffered runes access with unlimited unreads.
|
||||||
|
package buffruneio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"container/list"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rune to indicate end of file.
|
||||||
|
const (
|
||||||
|
EOF = -(iota + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNoRuneToUnread is returned by UnreadRune() when the read index is already at the beginning of the buffer.
|
||||||
|
var ErrNoRuneToUnread = errors.New("no rune to unwind")
|
||||||
|
|
||||||
|
// Reader implements runes buffering for an io.Reader object.
|
||||||
|
type Reader struct {
|
||||||
|
buffer *list.List
|
||||||
|
current *list.Element
|
||||||
|
input *bufio.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader returns a new Reader.
|
||||||
|
func NewReader(rd io.Reader) *Reader {
|
||||||
|
return &Reader{
|
||||||
|
buffer: list.New(),
|
||||||
|
input: bufio.NewReader(rd),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type runeWithSize struct {
|
||||||
|
r rune
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rd *Reader) feedBuffer() error {
|
||||||
|
r, size, err := rd.input.ReadRune()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r = EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
newRuneWithSize := runeWithSize{r, size}
|
||||||
|
|
||||||
|
rd.buffer.PushBack(newRuneWithSize)
|
||||||
|
if rd.current == nil {
|
||||||
|
rd.current = rd.buffer.Back()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadRune reads the next rune from buffer, or from the underlying reader if needed.
|
||||||
|
func (rd *Reader) ReadRune() (rune, int, error) {
|
||||||
|
if rd.current == rd.buffer.Back() || rd.current == nil {
|
||||||
|
err := rd.feedBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return EOF, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runeWithSize := rd.current.Value.(runeWithSize)
|
||||||
|
rd.current = rd.current.Next()
|
||||||
|
return runeWithSize.r, runeWithSize.size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnreadRune pushes back the previously read rune in the buffer, extending it if needed.
|
||||||
|
func (rd *Reader) UnreadRune() error {
|
||||||
|
if rd.current == rd.buffer.Front() {
|
||||||
|
return ErrNoRuneToUnread
|
||||||
|
}
|
||||||
|
if rd.current == nil {
|
||||||
|
rd.current = rd.buffer.Back()
|
||||||
|
} else {
|
||||||
|
rd.current = rd.current.Prev()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forget removes runes stored before the current stream position index.
|
||||||
|
func (rd *Reader) Forget() {
|
||||||
|
if rd.current == nil {
|
||||||
|
rd.current = rd.buffer.Back()
|
||||||
|
}
|
||||||
|
for ; rd.current != rd.buffer.Front(); rd.buffer.Remove(rd.current.Prev()) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeekRune returns at most the next n runes, reading from the uderlying source if
|
||||||
|
// needed. Does not move the current index. It includes EOF if reached.
|
||||||
|
func (rd *Reader) PeekRunes(n int) []rune {
|
||||||
|
res := make([]rune, 0, n)
|
||||||
|
cursor := rd.current
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if cursor == nil {
|
||||||
|
err := rd.feedBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
cursor = rd.buffer.Back()
|
||||||
|
}
|
||||||
|
if cursor != nil {
|
||||||
|
r := cursor.Value.(runeWithSize).r
|
||||||
|
res = append(res, r)
|
||||||
|
if r == EOF {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
cursor = cursor.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Package toml is a TOML parser and manipulation library.
|
||||||
|
//
|
||||||
|
// This version supports the specification as described in
|
||||||
|
// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md
|
||||||
|
//
|
||||||
|
// Marshaling
|
||||||
|
//
|
||||||
|
// Go-toml can marshal and unmarshal TOML documents from and to data
|
||||||
|
// structures.
|
||||||
|
//
|
||||||
|
// TOML document as a tree
|
||||||
|
//
|
||||||
|
// Go-toml can operate on a TOML document as a tree. Use one of the Load*
|
||||||
|
// functions to parse TOML data and obtain a Tree instance, then one of its
|
||||||
|
// methods to manipulate the tree.
|
||||||
|
//
|
||||||
|
// JSONPath-like queries
|
||||||
|
//
|
||||||
|
// The package github.com/pelletier/go-toml/query implements a system
|
||||||
|
// similar to JSONPath to quickly retrive elements of a TOML document using a
|
||||||
|
// single expression. See the package documentation for more information.
|
||||||
|
//
|
||||||
|
package toml
|
|
@ -0,0 +1,94 @@
|
||||||
|
// Parsing keys handling both bare and quoted keys.
|
||||||
|
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseKey(key string) ([]string, error) {
|
||||||
|
groups := []string{}
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
inQuotes := false
|
||||||
|
wasInQuotes := false
|
||||||
|
escapeNext := false
|
||||||
|
ignoreSpace := true
|
||||||
|
expectDot := false
|
||||||
|
|
||||||
|
for _, char := range key {
|
||||||
|
if ignoreSpace {
|
||||||
|
if char == ' ' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ignoreSpace = false
|
||||||
|
}
|
||||||
|
if escapeNext {
|
||||||
|
buffer.WriteRune(char)
|
||||||
|
escapeNext = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch char {
|
||||||
|
case '\\':
|
||||||
|
escapeNext = true
|
||||||
|
continue
|
||||||
|
case '"':
|
||||||
|
if inQuotes {
|
||||||
|
groups = append(groups, buffer.String())
|
||||||
|
buffer.Reset()
|
||||||
|
wasInQuotes = true
|
||||||
|
}
|
||||||
|
inQuotes = !inQuotes
|
||||||
|
expectDot = false
|
||||||
|
case '.':
|
||||||
|
if inQuotes {
|
||||||
|
buffer.WriteRune(char)
|
||||||
|
} else {
|
||||||
|
if !wasInQuotes {
|
||||||
|
if buffer.Len() == 0 {
|
||||||
|
return nil, errors.New("empty table key")
|
||||||
|
}
|
||||||
|
groups = append(groups, buffer.String())
|
||||||
|
buffer.Reset()
|
||||||
|
}
|
||||||
|
ignoreSpace = true
|
||||||
|
expectDot = false
|
||||||
|
wasInQuotes = false
|
||||||
|
}
|
||||||
|
case ' ':
|
||||||
|
if inQuotes {
|
||||||
|
buffer.WriteRune(char)
|
||||||
|
} else {
|
||||||
|
expectDot = true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if !inQuotes && !isValidBareChar(char) {
|
||||||
|
return nil, fmt.Errorf("invalid bare character: %c", char)
|
||||||
|
}
|
||||||
|
if !inQuotes && expectDot {
|
||||||
|
return nil, errors.New("what?")
|
||||||
|
}
|
||||||
|
buffer.WriteRune(char)
|
||||||
|
expectDot = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inQuotes {
|
||||||
|
return nil, errors.New("mismatched quotes")
|
||||||
|
}
|
||||||
|
if escapeNext {
|
||||||
|
return nil, errors.New("unfinished escape sequence")
|
||||||
|
}
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
groups = append(groups, buffer.String())
|
||||||
|
}
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return nil, errors.New("empty key")
|
||||||
|
}
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidBareChar(r rune) bool {
|
||||||
|
return isAlphanumeric(r) || r == '-' || unicode.IsNumber(r)
|
||||||
|
}
|
|
@ -0,0 +1,658 @@
|
||||||
|
// TOML lexer.
|
||||||
|
//
|
||||||
|
// Written using the principles developed by Rob Pike in
|
||||||
|
// http://www.youtube.com/watch?v=HxaD_trXwRE
|
||||||
|
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-buffruneio"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dateRegexp *regexp.Regexp
|
||||||
|
|
||||||
|
// Define state functions
|
||||||
|
type tomlLexStateFn func() tomlLexStateFn
|
||||||
|
|
||||||
|
// Define lexer
|
||||||
|
type tomlLexer struct {
|
||||||
|
input *buffruneio.Reader // Textual source
|
||||||
|
buffer bytes.Buffer // Runes composing the current token
|
||||||
|
tokens chan token
|
||||||
|
depth int
|
||||||
|
line int
|
||||||
|
col int
|
||||||
|
endbufferLine int
|
||||||
|
endbufferCol int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic read operations on input
|
||||||
|
|
||||||
|
func (l *tomlLexer) read() rune {
|
||||||
|
r, _, err := l.input.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if r == '\n' {
|
||||||
|
l.endbufferLine++
|
||||||
|
l.endbufferCol = 1
|
||||||
|
} else {
|
||||||
|
l.endbufferCol++
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) next() rune {
|
||||||
|
r := l.read()
|
||||||
|
|
||||||
|
if r != eof {
|
||||||
|
l.buffer.WriteRune(r)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) ignore() {
|
||||||
|
l.buffer.Reset()
|
||||||
|
l.line = l.endbufferLine
|
||||||
|
l.col = l.endbufferCol
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) skip() {
|
||||||
|
l.next()
|
||||||
|
l.ignore()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) fastForward(n int) {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
l.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) emitWithValue(t tokenType, value string) {
|
||||||
|
l.tokens <- token{
|
||||||
|
Position: Position{l.line, l.col},
|
||||||
|
typ: t,
|
||||||
|
val: value,
|
||||||
|
}
|
||||||
|
l.ignore()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) emit(t tokenType) {
|
||||||
|
l.emitWithValue(t, l.buffer.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) peek() rune {
|
||||||
|
r, _, err := l.input.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
l.input.UnreadRune()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) follow(next string) bool {
|
||||||
|
for _, expectedRune := range next {
|
||||||
|
r, _, err := l.input.ReadRune()
|
||||||
|
defer l.input.UnreadRune()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if expectedRune != r {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error management
|
||||||
|
|
||||||
|
func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn {
|
||||||
|
l.tokens <- token{
|
||||||
|
Position: Position{l.line, l.col},
|
||||||
|
typ: tokenError,
|
||||||
|
val: fmt.Sprintf(format, args...),
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// State functions
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexVoid() tomlLexStateFn {
|
||||||
|
for {
|
||||||
|
next := l.peek()
|
||||||
|
switch next {
|
||||||
|
case '[':
|
||||||
|
return l.lexTableKey
|
||||||
|
case '#':
|
||||||
|
return l.lexComment(l.lexVoid)
|
||||||
|
case '=':
|
||||||
|
return l.lexEqual
|
||||||
|
case '\r':
|
||||||
|
fallthrough
|
||||||
|
case '\n':
|
||||||
|
l.skip()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSpace(next) {
|
||||||
|
l.skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.depth > 0 {
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isKeyStartChar(next) {
|
||||||
|
return l.lexKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if next == eof {
|
||||||
|
l.next()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.emit(tokenEOF)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexRvalue() tomlLexStateFn {
|
||||||
|
for {
|
||||||
|
next := l.peek()
|
||||||
|
switch next {
|
||||||
|
case '.':
|
||||||
|
return l.errorf("cannot start float with a dot")
|
||||||
|
case '=':
|
||||||
|
return l.lexEqual
|
||||||
|
case '[':
|
||||||
|
l.depth++
|
||||||
|
return l.lexLeftBracket
|
||||||
|
case ']':
|
||||||
|
l.depth--
|
||||||
|
return l.lexRightBracket
|
||||||
|
case '{':
|
||||||
|
return l.lexLeftCurlyBrace
|
||||||
|
case '}':
|
||||||
|
return l.lexRightCurlyBrace
|
||||||
|
case '#':
|
||||||
|
return l.lexComment(l.lexRvalue)
|
||||||
|
case '"':
|
||||||
|
return l.lexString
|
||||||
|
case '\'':
|
||||||
|
return l.lexLiteralString
|
||||||
|
case ',':
|
||||||
|
return l.lexComma
|
||||||
|
case '\r':
|
||||||
|
fallthrough
|
||||||
|
case '\n':
|
||||||
|
l.skip()
|
||||||
|
if l.depth == 0 {
|
||||||
|
return l.lexVoid
|
||||||
|
}
|
||||||
|
return l.lexRvalue
|
||||||
|
case '_':
|
||||||
|
return l.errorf("cannot start number with underscore")
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.follow("true") {
|
||||||
|
return l.lexTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.follow("false") {
|
||||||
|
return l.lexFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSpace(next) {
|
||||||
|
l.skip()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if next == eof {
|
||||||
|
l.next()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
possibleDate := string(l.input.PeekRunes(35))
|
||||||
|
dateMatch := dateRegexp.FindString(possibleDate)
|
||||||
|
if dateMatch != "" {
|
||||||
|
l.fastForward(len(dateMatch))
|
||||||
|
return l.lexDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if next == '+' || next == '-' || isDigit(next) {
|
||||||
|
return l.lexNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAlphanumeric(next) {
|
||||||
|
return l.lexKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.errorf("no value can start with %c", next)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.emit(tokenEOF)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn {
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenLeftCurlyBrace)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn {
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenRightCurlyBrace)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexDate() tomlLexStateFn {
|
||||||
|
l.emit(tokenDate)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexTrue() tomlLexStateFn {
|
||||||
|
l.fastForward(4)
|
||||||
|
l.emit(tokenTrue)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexFalse() tomlLexStateFn {
|
||||||
|
l.fastForward(5)
|
||||||
|
l.emit(tokenFalse)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexEqual() tomlLexStateFn {
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenEqual)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexComma() tomlLexStateFn {
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenComma)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexKey() tomlLexStateFn {
|
||||||
|
growingString := ""
|
||||||
|
|
||||||
|
for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() {
|
||||||
|
if r == '"' {
|
||||||
|
l.next()
|
||||||
|
str, err := l.lexStringAsString(`"`, false, true)
|
||||||
|
if err != nil {
|
||||||
|
return l.errorf(err.Error())
|
||||||
|
}
|
||||||
|
growingString += `"` + str + `"`
|
||||||
|
l.next()
|
||||||
|
continue
|
||||||
|
} else if r == '\n' {
|
||||||
|
return l.errorf("keys cannot contain new lines")
|
||||||
|
} else if isSpace(r) {
|
||||||
|
break
|
||||||
|
} else if !isValidBareChar(r) {
|
||||||
|
return l.errorf("keys cannot contain %c character", r)
|
||||||
|
}
|
||||||
|
growingString += string(r)
|
||||||
|
l.next()
|
||||||
|
}
|
||||||
|
l.emitWithValue(tokenKey, growingString)
|
||||||
|
return l.lexVoid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexComment(previousState tomlLexStateFn) tomlLexStateFn {
|
||||||
|
return func() tomlLexStateFn {
|
||||||
|
for next := l.peek(); next != '\n' && next != eof; next = l.peek() {
|
||||||
|
if next == '\r' && l.follow("\r\n") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
}
|
||||||
|
l.ignore()
|
||||||
|
return previousState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexLeftBracket() tomlLexStateFn {
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenLeftBracket)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) {
|
||||||
|
growingString := ""
|
||||||
|
|
||||||
|
if discardLeadingNewLine {
|
||||||
|
if l.follow("\r\n") {
|
||||||
|
l.skip()
|
||||||
|
l.skip()
|
||||||
|
} else if l.peek() == '\n' {
|
||||||
|
l.skip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find end of string
|
||||||
|
for {
|
||||||
|
if l.follow(terminator) {
|
||||||
|
return growingString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
next := l.peek()
|
||||||
|
if next == eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
growingString += string(l.next())
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("unclosed string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexLiteralString() tomlLexStateFn {
|
||||||
|
l.skip()
|
||||||
|
|
||||||
|
// handle special case for triple-quote
|
||||||
|
terminator := "'"
|
||||||
|
discardLeadingNewLine := false
|
||||||
|
if l.follow("''") {
|
||||||
|
l.skip()
|
||||||
|
l.skip()
|
||||||
|
terminator = "'''"
|
||||||
|
discardLeadingNewLine = true
|
||||||
|
}
|
||||||
|
|
||||||
|
str, err := l.lexLiteralStringAsString(terminator, discardLeadingNewLine)
|
||||||
|
if err != nil {
|
||||||
|
return l.errorf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
l.emitWithValue(tokenString, str)
|
||||||
|
l.fastForward(len(terminator))
|
||||||
|
l.ignore()
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lex a string and return the results as a string.
|
||||||
|
// Terminator is the substring indicating the end of the token.
|
||||||
|
// The resulting string does not include the terminator.
|
||||||
|
func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine, acceptNewLines bool) (string, error) {
|
||||||
|
growingString := ""
|
||||||
|
|
||||||
|
if discardLeadingNewLine {
|
||||||
|
if l.follow("\r\n") {
|
||||||
|
l.skip()
|
||||||
|
l.skip()
|
||||||
|
} else if l.peek() == '\n' {
|
||||||
|
l.skip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if l.follow(terminator) {
|
||||||
|
return growingString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.follow("\\") {
|
||||||
|
l.next()
|
||||||
|
switch l.peek() {
|
||||||
|
case '\r':
|
||||||
|
fallthrough
|
||||||
|
case '\n':
|
||||||
|
fallthrough
|
||||||
|
case '\t':
|
||||||
|
fallthrough
|
||||||
|
case ' ':
|
||||||
|
// skip all whitespace chars following backslash
|
||||||
|
for strings.ContainsRune("\r\n\t ", l.peek()) {
|
||||||
|
l.next()
|
||||||
|
}
|
||||||
|
case '"':
|
||||||
|
growingString += "\""
|
||||||
|
l.next()
|
||||||
|
case 'n':
|
||||||
|
growingString += "\n"
|
||||||
|
l.next()
|
||||||
|
case 'b':
|
||||||
|
growingString += "\b"
|
||||||
|
l.next()
|
||||||
|
case 'f':
|
||||||
|
growingString += "\f"
|
||||||
|
l.next()
|
||||||
|
case '/':
|
||||||
|
growingString += "/"
|
||||||
|
l.next()
|
||||||
|
case 't':
|
||||||
|
growingString += "\t"
|
||||||
|
l.next()
|
||||||
|
case 'r':
|
||||||
|
growingString += "\r"
|
||||||
|
l.next()
|
||||||
|
case '\\':
|
||||||
|
growingString += "\\"
|
||||||
|
l.next()
|
||||||
|
case 'u':
|
||||||
|
l.next()
|
||||||
|
code := ""
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
c := l.peek()
|
||||||
|
if !isHexDigit(c) {
|
||||||
|
return "", errors.New("unfinished unicode escape")
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
code = code + string(c)
|
||||||
|
}
|
||||||
|
intcode, err := strconv.ParseInt(code, 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("invalid unicode escape: \\u" + code)
|
||||||
|
}
|
||||||
|
growingString += string(rune(intcode))
|
||||||
|
case 'U':
|
||||||
|
l.next()
|
||||||
|
code := ""
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
c := l.peek()
|
||||||
|
if !isHexDigit(c) {
|
||||||
|
return "", errors.New("unfinished unicode escape")
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
code = code + string(c)
|
||||||
|
}
|
||||||
|
intcode, err := strconv.ParseInt(code, 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("invalid unicode escape: \\U" + code)
|
||||||
|
}
|
||||||
|
growingString += string(rune(intcode))
|
||||||
|
default:
|
||||||
|
return "", errors.New("invalid escape sequence: \\" + string(l.peek()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r := l.peek()
|
||||||
|
|
||||||
|
if 0x00 <= r && r <= 0x1F && !(acceptNewLines && (r == '\n' || r == '\r')) {
|
||||||
|
return "", fmt.Errorf("unescaped control character %U", r)
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
growingString += string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.peek() == eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("unclosed string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexString() tomlLexStateFn {
|
||||||
|
l.skip()
|
||||||
|
|
||||||
|
// handle special case for triple-quote
|
||||||
|
terminator := `"`
|
||||||
|
discardLeadingNewLine := false
|
||||||
|
acceptNewLines := false
|
||||||
|
if l.follow(`""`) {
|
||||||
|
l.skip()
|
||||||
|
l.skip()
|
||||||
|
terminator = `"""`
|
||||||
|
discardLeadingNewLine = true
|
||||||
|
acceptNewLines = true
|
||||||
|
}
|
||||||
|
|
||||||
|
str, err := l.lexStringAsString(terminator, discardLeadingNewLine, acceptNewLines)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return l.errorf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
l.emitWithValue(tokenString, str)
|
||||||
|
l.fastForward(len(terminator))
|
||||||
|
l.ignore()
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexTableKey() tomlLexStateFn {
|
||||||
|
l.next()
|
||||||
|
|
||||||
|
if l.peek() == '[' {
|
||||||
|
// token '[[' signifies an array of tables
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenDoubleLeftBracket)
|
||||||
|
return l.lexInsideTableArrayKey
|
||||||
|
}
|
||||||
|
// vanilla table key
|
||||||
|
l.emit(tokenLeftBracket)
|
||||||
|
return l.lexInsideTableKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexInsideTableArrayKey() tomlLexStateFn {
|
||||||
|
for r := l.peek(); r != eof; r = l.peek() {
|
||||||
|
switch r {
|
||||||
|
case ']':
|
||||||
|
if l.buffer.Len() > 0 {
|
||||||
|
l.emit(tokenKeyGroupArray)
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
if l.peek() != ']' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenDoubleRightBracket)
|
||||||
|
return l.lexVoid
|
||||||
|
case '[':
|
||||||
|
return l.errorf("table array key cannot contain ']'")
|
||||||
|
default:
|
||||||
|
l.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l.errorf("unclosed table array key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexInsideTableKey() tomlLexStateFn {
|
||||||
|
for r := l.peek(); r != eof; r = l.peek() {
|
||||||
|
switch r {
|
||||||
|
case ']':
|
||||||
|
if l.buffer.Len() > 0 {
|
||||||
|
l.emit(tokenKeyGroup)
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenRightBracket)
|
||||||
|
return l.lexVoid
|
||||||
|
case '[':
|
||||||
|
return l.errorf("table key cannot contain ']'")
|
||||||
|
default:
|
||||||
|
l.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l.errorf("unclosed table key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexRightBracket() tomlLexStateFn {
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenRightBracket)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexNumber() tomlLexStateFn {
|
||||||
|
r := l.peek()
|
||||||
|
if r == '+' || r == '-' {
|
||||||
|
l.next()
|
||||||
|
}
|
||||||
|
pointSeen := false
|
||||||
|
expSeen := false
|
||||||
|
digitSeen := false
|
||||||
|
for {
|
||||||
|
next := l.peek()
|
||||||
|
if next == '.' {
|
||||||
|
if pointSeen {
|
||||||
|
return l.errorf("cannot have two dots in one float")
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
if !isDigit(l.peek()) {
|
||||||
|
return l.errorf("float cannot end with a dot")
|
||||||
|
}
|
||||||
|
pointSeen = true
|
||||||
|
} else if next == 'e' || next == 'E' {
|
||||||
|
expSeen = true
|
||||||
|
l.next()
|
||||||
|
r := l.peek()
|
||||||
|
if r == '+' || r == '-' {
|
||||||
|
l.next()
|
||||||
|
}
|
||||||
|
} else if isDigit(next) {
|
||||||
|
digitSeen = true
|
||||||
|
l.next()
|
||||||
|
} else if next == '_' {
|
||||||
|
l.next()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if pointSeen && !digitSeen {
|
||||||
|
return l.errorf("cannot start float with a dot")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !digitSeen {
|
||||||
|
return l.errorf("no digit in that number")
|
||||||
|
}
|
||||||
|
if pointSeen || expSeen {
|
||||||
|
l.emit(tokenFloat)
|
||||||
|
} else {
|
||||||
|
l.emit(tokenInteger)
|
||||||
|
}
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) run() {
|
||||||
|
for state := l.lexVoid; state != nil; {
|
||||||
|
state = state()
|
||||||
|
}
|
||||||
|
close(l.tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dateRegexp = regexp.MustCompile(`^\d{1,4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry point
|
||||||
|
func lexToml(input io.Reader) chan token {
|
||||||
|
bufferedInput := buffruneio.NewReader(input)
|
||||||
|
l := &tomlLexer{
|
||||||
|
input: bufferedInput,
|
||||||
|
tokens: make(chan token),
|
||||||
|
line: 1,
|
||||||
|
col: 1,
|
||||||
|
endbufferLine: 1,
|
||||||
|
endbufferCol: 1,
|
||||||
|
}
|
||||||
|
go l.run()
|
||||||
|
return l.tokens
|
||||||
|
}
|
|
@ -0,0 +1,484 @@
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tomlOpts struct {
|
||||||
|
name string
|
||||||
|
include bool
|
||||||
|
omitempty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeType = reflect.TypeOf(time.Time{})
|
||||||
|
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()
|
||||||
|
|
||||||
|
// Check if the given marshall type maps to a Tree primitive
|
||||||
|
func isPrimitive(mtype reflect.Type) bool {
|
||||||
|
switch mtype.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return isPrimitive(mtype.Elem())
|
||||||
|
case reflect.Bool:
|
||||||
|
return true
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return true
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return true
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return true
|
||||||
|
case reflect.String:
|
||||||
|
return true
|
||||||
|
case reflect.Struct:
|
||||||
|
return mtype == timeType || isCustomMarshaler(mtype)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the given marshall type maps to a Tree slice
|
||||||
|
func isTreeSlice(mtype reflect.Type) bool {
|
||||||
|
switch mtype.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return !isOtherSlice(mtype)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the given marshall type maps to a non-Tree slice
|
||||||
|
func isOtherSlice(mtype reflect.Type) bool {
|
||||||
|
switch mtype.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return isOtherSlice(mtype.Elem())
|
||||||
|
case reflect.Slice:
|
||||||
|
return isPrimitive(mtype.Elem()) || isOtherSlice(mtype.Elem())
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the given marshall type maps to a Tree
|
||||||
|
func isTree(mtype reflect.Type) bool {
|
||||||
|
switch mtype.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
return true
|
||||||
|
case reflect.Struct:
|
||||||
|
return !isPrimitive(mtype)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCustomMarshaler(mtype reflect.Type) bool {
|
||||||
|
return mtype.Implements(marshalerType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func callCustomMarshaler(mval reflect.Value) ([]byte, error) {
|
||||||
|
return mval.Interface().(Marshaler).MarshalTOML()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshaler is the interface implemented by types that
|
||||||
|
// can marshal themselves into valid TOML.
|
||||||
|
type Marshaler interface {
|
||||||
|
MarshalTOML() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Marshal returns the TOML encoding of v. Behavior is similar to the Go json
|
||||||
|
encoder, except that there is no concept of a Marshaler interface or MarshalTOML
|
||||||
|
function for sub-structs, and currently only definite types can be marshaled
|
||||||
|
(i.e. no `interface{}`).
|
||||||
|
|
||||||
|
Note that pointers are automatically assigned the "omitempty" option, as TOML
|
||||||
|
explicity does not handle null values (saying instead the label should be
|
||||||
|
dropped).
|
||||||
|
|
||||||
|
Tree structural types and corresponding marshal types:
|
||||||
|
|
||||||
|
*Tree (*)struct, (*)map[string]interface{}
|
||||||
|
[]*Tree (*)[](*)struct, (*)[](*)map[string]interface{}
|
||||||
|
[]interface{} (as interface{}) (*)[]primitive, (*)[]([]interface{})
|
||||||
|
interface{} (*)primitive
|
||||||
|
|
||||||
|
Tree primitive types and corresponding marshal types:
|
||||||
|
|
||||||
|
uint64 uint, uint8-uint64, pointers to same
|
||||||
|
int64 int, int8-uint64, pointers to same
|
||||||
|
float64 float32, float64, pointers to same
|
||||||
|
string string, pointers to same
|
||||||
|
bool bool, pointers to same
|
||||||
|
time.Time time.Time{}, pointers to same
|
||||||
|
*/
|
||||||
|
func Marshal(v interface{}) ([]byte, error) {
|
||||||
|
mtype := reflect.TypeOf(v)
|
||||||
|
if mtype.Kind() != reflect.Struct {
|
||||||
|
return []byte{}, errors.New("Only a struct can be marshaled to TOML")
|
||||||
|
}
|
||||||
|
sval := reflect.ValueOf(v)
|
||||||
|
if isCustomMarshaler(mtype) {
|
||||||
|
return callCustomMarshaler(sval)
|
||||||
|
}
|
||||||
|
t, err := valueToTree(mtype, sval)
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
s, err := t.ToTomlString()
|
||||||
|
return []byte(s), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert given marshal struct or map value to toml tree
|
||||||
|
func valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, error) {
|
||||||
|
if mtype.Kind() == reflect.Ptr {
|
||||||
|
return valueToTree(mtype.Elem(), mval.Elem())
|
||||||
|
}
|
||||||
|
tval := newTree()
|
||||||
|
switch mtype.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
for i := 0; i < mtype.NumField(); i++ {
|
||||||
|
mtypef, mvalf := mtype.Field(i), mval.Field(i)
|
||||||
|
opts := tomlOptions(mtypef)
|
||||||
|
if opts.include && (!opts.omitempty || !isZero(mvalf)) {
|
||||||
|
val, err := valueToToml(mtypef.Type, mvalf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tval.Set(opts.name, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
for _, key := range mval.MapKeys() {
|
||||||
|
mvalf := mval.MapIndex(key)
|
||||||
|
val, err := valueToToml(mtype.Elem(), mvalf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tval.Set(key.String(), val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert given marshal slice to slice of Toml trees
|
||||||
|
func valueToTreeSlice(mtype reflect.Type, mval reflect.Value) ([]*Tree, error) {
|
||||||
|
tval := make([]*Tree, mval.Len(), mval.Len())
|
||||||
|
for i := 0; i < mval.Len(); i++ {
|
||||||
|
val, err := valueToTree(mtype.Elem(), mval.Index(i))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tval[i] = val
|
||||||
|
}
|
||||||
|
return tval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert given marshal slice to slice of toml values
|
||||||
|
func valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
|
||||||
|
tval := make([]interface{}, mval.Len(), mval.Len())
|
||||||
|
for i := 0; i < mval.Len(); i++ {
|
||||||
|
val, err := valueToToml(mtype.Elem(), mval.Index(i))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tval[i] = val
|
||||||
|
}
|
||||||
|
return tval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert given marshal value to toml value
|
||||||
|
func valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
|
||||||
|
if mtype.Kind() == reflect.Ptr {
|
||||||
|
return valueToToml(mtype.Elem(), mval.Elem())
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case isCustomMarshaler(mtype):
|
||||||
|
return callCustomMarshaler(mval)
|
||||||
|
case isTree(mtype):
|
||||||
|
return valueToTree(mtype, mval)
|
||||||
|
case isTreeSlice(mtype):
|
||||||
|
return valueToTreeSlice(mtype, mval)
|
||||||
|
case isOtherSlice(mtype):
|
||||||
|
return valueToOtherSlice(mtype, mval)
|
||||||
|
default:
|
||||||
|
switch mtype.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return mval.Bool(), nil
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return mval.Int(), nil
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return mval.Uint(), nil
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return mval.Float(), nil
|
||||||
|
case reflect.String:
|
||||||
|
return mval.String(), nil
|
||||||
|
case reflect.Struct:
|
||||||
|
return mval.Interface().(time.Time), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Marshal can't handle %v(%v)", mtype, mtype.Kind())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal attempts to unmarshal the Tree into a Go struct pointed by v.
|
||||||
|
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
|
||||||
|
// sub-structs, and only definite types can be unmarshaled.
|
||||||
|
func (t *Tree) Unmarshal(v interface{}) error {
|
||||||
|
mtype := reflect.TypeOf(v)
|
||||||
|
if mtype.Kind() != reflect.Ptr || mtype.Elem().Kind() != reflect.Struct {
|
||||||
|
return errors.New("Only a pointer to struct can be unmarshaled from TOML")
|
||||||
|
}
|
||||||
|
|
||||||
|
sval, err := valueFromTree(mtype.Elem(), t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reflect.ValueOf(v).Elem().Set(sval)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal parses the TOML-encoded data and stores the result in the value
|
||||||
|
// pointed to by v. Behavior is similar to the Go json encoder, except that there
|
||||||
|
// is no concept of an Unmarshaler interface or UnmarshalTOML function for
|
||||||
|
// sub-structs, and currently only definite types can be unmarshaled to (i.e. no
|
||||||
|
// `interface{}`).
|
||||||
|
//
|
||||||
|
// See Marshal() documentation for types mapping table.
|
||||||
|
func Unmarshal(data []byte, v interface{}) error {
|
||||||
|
t, err := LoadReader(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return t.Unmarshal(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert toml tree to marshal struct or map, using marshal type
|
||||||
|
func valueFromTree(mtype reflect.Type, tval *Tree) (reflect.Value, error) {
|
||||||
|
if mtype.Kind() == reflect.Ptr {
|
||||||
|
return unwrapPointer(mtype, tval)
|
||||||
|
}
|
||||||
|
var mval reflect.Value
|
||||||
|
switch mtype.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
mval = reflect.New(mtype).Elem()
|
||||||
|
for i := 0; i < mtype.NumField(); i++ {
|
||||||
|
mtypef := mtype.Field(i)
|
||||||
|
opts := tomlOptions(mtypef)
|
||||||
|
if opts.include {
|
||||||
|
key := opts.name
|
||||||
|
exists := tval.Has(key)
|
||||||
|
if exists {
|
||||||
|
val := tval.Get(key)
|
||||||
|
mvalf, err := valueFromToml(mtypef.Type, val)
|
||||||
|
if err != nil {
|
||||||
|
return mval, formatError(err, tval.GetPosition(key))
|
||||||
|
}
|
||||||
|
mval.Field(i).Set(mvalf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
mval = reflect.MakeMap(mtype)
|
||||||
|
for _, key := range tval.Keys() {
|
||||||
|
val := tval.Get(key)
|
||||||
|
mvalf, err := valueFromToml(mtype.Elem(), val)
|
||||||
|
if err != nil {
|
||||||
|
return mval, formatError(err, tval.GetPosition(key))
|
||||||
|
}
|
||||||
|
mval.SetMapIndex(reflect.ValueOf(key), mvalf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert toml value to marshal struct/map slice, using marshal type
|
||||||
|
func valueFromTreeSlice(mtype reflect.Type, tval []*Tree) (reflect.Value, error) {
|
||||||
|
mval := reflect.MakeSlice(mtype, len(tval), len(tval))
|
||||||
|
for i := 0; i < len(tval); i++ {
|
||||||
|
val, err := valueFromTree(mtype.Elem(), tval[i])
|
||||||
|
if err != nil {
|
||||||
|
return mval, err
|
||||||
|
}
|
||||||
|
mval.Index(i).Set(val)
|
||||||
|
}
|
||||||
|
return mval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert toml value to marshal primitive slice, using marshal type
|
||||||
|
func valueFromOtherSlice(mtype reflect.Type, tval []interface{}) (reflect.Value, error) {
|
||||||
|
mval := reflect.MakeSlice(mtype, len(tval), len(tval))
|
||||||
|
for i := 0; i < len(tval); i++ {
|
||||||
|
val, err := valueFromToml(mtype.Elem(), tval[i])
|
||||||
|
if err != nil {
|
||||||
|
return mval, err
|
||||||
|
}
|
||||||
|
mval.Index(i).Set(val)
|
||||||
|
}
|
||||||
|
return mval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert toml value to marshal value, using marshal type
|
||||||
|
func valueFromToml(mtype reflect.Type, tval interface{}) (reflect.Value, error) {
|
||||||
|
if mtype.Kind() == reflect.Ptr {
|
||||||
|
return unwrapPointer(mtype, tval)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case isTree(mtype):
|
||||||
|
return valueFromTree(mtype, tval.(*Tree))
|
||||||
|
case isTreeSlice(mtype):
|
||||||
|
return valueFromTreeSlice(mtype, tval.([]*Tree))
|
||||||
|
case isOtherSlice(mtype):
|
||||||
|
return valueFromOtherSlice(mtype, tval.([]interface{}))
|
||||||
|
default:
|
||||||
|
switch mtype.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
val, ok := tval.(bool)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to bool", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(val), nil
|
||||||
|
case reflect.Int:
|
||||||
|
val, ok := tval.(int64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to int", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(int(val)), nil
|
||||||
|
case reflect.Int8:
|
||||||
|
val, ok := tval.(int64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to int", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(int8(val)), nil
|
||||||
|
case reflect.Int16:
|
||||||
|
val, ok := tval.(int64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to int", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(int16(val)), nil
|
||||||
|
case reflect.Int32:
|
||||||
|
val, ok := tval.(int64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to int", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(int32(val)), nil
|
||||||
|
case reflect.Int64:
|
||||||
|
val, ok := tval.(int64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to int", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(val), nil
|
||||||
|
case reflect.Uint:
|
||||||
|
val, ok := tval.(int64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to uint", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(uint(val)), nil
|
||||||
|
case reflect.Uint8:
|
||||||
|
val, ok := tval.(int64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to uint", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(uint8(val)), nil
|
||||||
|
case reflect.Uint16:
|
||||||
|
val, ok := tval.(int64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to uint", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(uint16(val)), nil
|
||||||
|
case reflect.Uint32:
|
||||||
|
val, ok := tval.(int64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to uint", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(uint32(val)), nil
|
||||||
|
case reflect.Uint64:
|
||||||
|
val, ok := tval.(int64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to uint", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(uint64(val)), nil
|
||||||
|
case reflect.Float32:
|
||||||
|
val, ok := tval.(float64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to float", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(float32(val)), nil
|
||||||
|
case reflect.Float64:
|
||||||
|
val, ok := tval.(float64)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to float", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(val), nil
|
||||||
|
case reflect.String:
|
||||||
|
val, ok := tval.(string)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to string", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(val), nil
|
||||||
|
case reflect.Struct:
|
||||||
|
val, ok := tval.(time.Time)
|
||||||
|
if !ok {
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to time", tval, tval)
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(val), nil
|
||||||
|
default:
|
||||||
|
return reflect.ValueOf(nil), fmt.Errorf("Unmarshal can't handle %v(%v)", mtype, mtype.Kind())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unwrapPointer(mtype reflect.Type, tval interface{}) (reflect.Value, error) {
|
||||||
|
val, err := valueFromToml(mtype.Elem(), tval)
|
||||||
|
if err != nil {
|
||||||
|
return reflect.ValueOf(nil), err
|
||||||
|
}
|
||||||
|
mval := reflect.New(mtype.Elem())
|
||||||
|
mval.Elem().Set(val)
|
||||||
|
return mval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tomlOptions(vf reflect.StructField) tomlOpts {
|
||||||
|
tag := vf.Tag.Get("toml")
|
||||||
|
parse := strings.Split(tag, ",")
|
||||||
|
result := tomlOpts{vf.Name, true, false}
|
||||||
|
if parse[0] != "" {
|
||||||
|
if parse[0] == "-" && len(parse) == 1 {
|
||||||
|
result.include = false
|
||||||
|
} else {
|
||||||
|
result.name = strings.Trim(parse[0], " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if vf.PkgPath != "" {
|
||||||
|
result.include = false
|
||||||
|
}
|
||||||
|
if len(parse) > 1 && strings.Trim(parse[1], " ") == "omitempty" {
|
||||||
|
result.omitempty = true
|
||||||
|
}
|
||||||
|
if vf.Type.Kind() == reflect.Ptr {
|
||||||
|
result.omitempty = true
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func isZero(val reflect.Value) bool {
|
||||||
|
switch val.Type().Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Array:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Slice:
|
||||||
|
return val.Len() == 0
|
||||||
|
default:
|
||||||
|
return reflect.DeepEqual(val.Interface(), reflect.Zero(val.Type()).Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatError(err error, pos Position) error {
|
||||||
|
if err.Error()[0] == '(' { // Error already contains position information
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s: %s", pos, err)
|
||||||
|
}
|
|
@ -0,0 +1,393 @@
|
||||||
|
// TOML Parser.
|
||||||
|
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tomlParser struct {
|
||||||
|
flow chan token
|
||||||
|
tree *Tree
|
||||||
|
tokensBuffer []token
|
||||||
|
currentTable []string
|
||||||
|
seenTableKeys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type tomlParserStateFn func() tomlParserStateFn
|
||||||
|
|
||||||
|
// Formats and panics an error message based on a token
|
||||||
|
func (p *tomlParser) raiseError(tok *token, msg string, args ...interface{}) {
|
||||||
|
panic(tok.Position.String() + ": " + fmt.Sprintf(msg, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) run() {
|
||||||
|
for state := p.parseStart; state != nil; {
|
||||||
|
state = state()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) peek() *token {
|
||||||
|
if len(p.tokensBuffer) != 0 {
|
||||||
|
return &(p.tokensBuffer[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, ok := <-p.flow
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.tokensBuffer = append(p.tokensBuffer, tok)
|
||||||
|
return &tok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) assume(typ tokenType) {
|
||||||
|
tok := p.getToken()
|
||||||
|
if tok == nil {
|
||||||
|
p.raiseError(tok, "was expecting token %s, but token stream is empty", tok)
|
||||||
|
}
|
||||||
|
if tok.typ != typ {
|
||||||
|
p.raiseError(tok, "was expecting token %s, but got %s instead", typ, tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) getToken() *token {
|
||||||
|
if len(p.tokensBuffer) != 0 {
|
||||||
|
tok := p.tokensBuffer[0]
|
||||||
|
p.tokensBuffer = p.tokensBuffer[1:]
|
||||||
|
return &tok
|
||||||
|
}
|
||||||
|
tok, ok := <-p.flow
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &tok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) parseStart() tomlParserStateFn {
|
||||||
|
tok := p.peek()
|
||||||
|
|
||||||
|
// end of stream, parsing is finished
|
||||||
|
if tok == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tok.typ {
|
||||||
|
case tokenDoubleLeftBracket:
|
||||||
|
return p.parseGroupArray
|
||||||
|
case tokenLeftBracket:
|
||||||
|
return p.parseGroup
|
||||||
|
case tokenKey:
|
||||||
|
return p.parseAssign
|
||||||
|
case tokenEOF:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
p.raiseError(tok, "unexpected token")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) parseGroupArray() tomlParserStateFn {
|
||||||
|
startToken := p.getToken() // discard the [[
|
||||||
|
key := p.getToken()
|
||||||
|
if key.typ != tokenKeyGroupArray {
|
||||||
|
p.raiseError(key, "unexpected token %s, was expecting a table array key", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get or create table array element at the indicated part in the path
|
||||||
|
keys, err := parseKey(key.val)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(key, "invalid table array key: %s", err)
|
||||||
|
}
|
||||||
|
p.tree.createSubTree(keys[:len(keys)-1], startToken.Position) // create parent entries
|
||||||
|
destTree := p.tree.GetPath(keys)
|
||||||
|
var array []*Tree
|
||||||
|
if destTree == nil {
|
||||||
|
array = make([]*Tree, 0)
|
||||||
|
} else if target, ok := destTree.([]*Tree); ok && target != nil {
|
||||||
|
array = destTree.([]*Tree)
|
||||||
|
} else {
|
||||||
|
p.raiseError(key, "key %s is already assigned and not of type table array", key)
|
||||||
|
}
|
||||||
|
p.currentTable = keys
|
||||||
|
|
||||||
|
// add a new tree to the end of the table array
|
||||||
|
newTree := newTree()
|
||||||
|
newTree.position = startToken.Position
|
||||||
|
array = append(array, newTree)
|
||||||
|
p.tree.SetPath(p.currentTable, array)
|
||||||
|
|
||||||
|
// remove all keys that were children of this table array
|
||||||
|
prefix := key.val + "."
|
||||||
|
found := false
|
||||||
|
for ii := 0; ii < len(p.seenTableKeys); {
|
||||||
|
tableKey := p.seenTableKeys[ii]
|
||||||
|
if strings.HasPrefix(tableKey, prefix) {
|
||||||
|
p.seenTableKeys = append(p.seenTableKeys[:ii], p.seenTableKeys[ii+1:]...)
|
||||||
|
} else {
|
||||||
|
found = (tableKey == key.val)
|
||||||
|
ii++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep this key name from use by other kinds of assignments
|
||||||
|
if !found {
|
||||||
|
p.seenTableKeys = append(p.seenTableKeys, key.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// move to next parser state
|
||||||
|
p.assume(tokenDoubleRightBracket)
|
||||||
|
return p.parseStart
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) parseGroup() tomlParserStateFn {
|
||||||
|
startToken := p.getToken() // discard the [
|
||||||
|
key := p.getToken()
|
||||||
|
if key.typ != tokenKeyGroup {
|
||||||
|
p.raiseError(key, "unexpected token %s, was expecting a table key", key)
|
||||||
|
}
|
||||||
|
for _, item := range p.seenTableKeys {
|
||||||
|
if item == key.val {
|
||||||
|
p.raiseError(key, "duplicated tables")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.seenTableKeys = append(p.seenTableKeys, key.val)
|
||||||
|
keys, err := parseKey(key.val)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(key, "invalid table array key: %s", err)
|
||||||
|
}
|
||||||
|
if err := p.tree.createSubTree(keys, startToken.Position); err != nil {
|
||||||
|
p.raiseError(key, "%s", err)
|
||||||
|
}
|
||||||
|
p.assume(tokenRightBracket)
|
||||||
|
p.currentTable = keys
|
||||||
|
return p.parseStart
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) parseAssign() tomlParserStateFn {
|
||||||
|
key := p.getToken()
|
||||||
|
p.assume(tokenEqual)
|
||||||
|
|
||||||
|
value := p.parseRvalue()
|
||||||
|
var tableKey []string
|
||||||
|
if len(p.currentTable) > 0 {
|
||||||
|
tableKey = p.currentTable
|
||||||
|
} else {
|
||||||
|
tableKey = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the table to assign, looking out for arrays of tables
|
||||||
|
var targetNode *Tree
|
||||||
|
switch node := p.tree.GetPath(tableKey).(type) {
|
||||||
|
case []*Tree:
|
||||||
|
targetNode = node[len(node)-1]
|
||||||
|
case *Tree:
|
||||||
|
targetNode = node
|
||||||
|
default:
|
||||||
|
p.raiseError(key, "Unknown table type for path: %s",
|
||||||
|
strings.Join(tableKey, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign value to the found table
|
||||||
|
keyVals, err := parseKey(key.val)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(key, "%s", err)
|
||||||
|
}
|
||||||
|
if len(keyVals) != 1 {
|
||||||
|
p.raiseError(key, "Invalid key")
|
||||||
|
}
|
||||||
|
keyVal := keyVals[0]
|
||||||
|
localKey := []string{keyVal}
|
||||||
|
finalKey := append(tableKey, keyVal)
|
||||||
|
if targetNode.GetPath(localKey) != nil {
|
||||||
|
p.raiseError(key, "The following key was defined twice: %s",
|
||||||
|
strings.Join(finalKey, "."))
|
||||||
|
}
|
||||||
|
var toInsert interface{}
|
||||||
|
|
||||||
|
switch value.(type) {
|
||||||
|
case *Tree, []*Tree:
|
||||||
|
toInsert = value
|
||||||
|
default:
|
||||||
|
toInsert = &tomlValue{value, key.Position}
|
||||||
|
}
|
||||||
|
targetNode.values[keyVal] = toInsert
|
||||||
|
return p.parseStart
|
||||||
|
}
|
||||||
|
|
||||||
|
var numberUnderscoreInvalidRegexp *regexp.Regexp
|
||||||
|
|
||||||
|
func cleanupNumberToken(value string) (string, error) {
|
||||||
|
if numberUnderscoreInvalidRegexp.MatchString(value) {
|
||||||
|
return "", errors.New("invalid use of _ in number")
|
||||||
|
}
|
||||||
|
cleanedVal := strings.Replace(value, "_", "", -1)
|
||||||
|
return cleanedVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) parseRvalue() interface{} {
|
||||||
|
tok := p.getToken()
|
||||||
|
if tok == nil || tok.typ == tokenEOF {
|
||||||
|
p.raiseError(tok, "expecting a value")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tok.typ {
|
||||||
|
case tokenString:
|
||||||
|
return tok.val
|
||||||
|
case tokenTrue:
|
||||||
|
return true
|
||||||
|
case tokenFalse:
|
||||||
|
return false
|
||||||
|
case tokenInteger:
|
||||||
|
cleanedVal, err := cleanupNumberToken(tok.val)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(tok, "%s", err)
|
||||||
|
}
|
||||||
|
val, err := strconv.ParseInt(cleanedVal, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(tok, "%s", err)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
case tokenFloat:
|
||||||
|
cleanedVal, err := cleanupNumberToken(tok.val)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(tok, "%s", err)
|
||||||
|
}
|
||||||
|
val, err := strconv.ParseFloat(cleanedVal, 64)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(tok, "%s", err)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
case tokenDate:
|
||||||
|
val, err := time.ParseInLocation(time.RFC3339Nano, tok.val, time.UTC)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(tok, "%s", err)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
case tokenLeftBracket:
|
||||||
|
return p.parseArray()
|
||||||
|
case tokenLeftCurlyBrace:
|
||||||
|
return p.parseInlineTable()
|
||||||
|
case tokenEqual:
|
||||||
|
p.raiseError(tok, "cannot have multiple equals for the same key")
|
||||||
|
case tokenError:
|
||||||
|
p.raiseError(tok, "%s", tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.raiseError(tok, "never reached")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenIsComma(t *token) bool {
|
||||||
|
return t != nil && t.typ == tokenComma
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) parseInlineTable() *Tree {
|
||||||
|
tree := newTree()
|
||||||
|
var previous *token
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
follow := p.peek()
|
||||||
|
if follow == nil || follow.typ == tokenEOF {
|
||||||
|
p.raiseError(follow, "unterminated inline table")
|
||||||
|
}
|
||||||
|
switch follow.typ {
|
||||||
|
case tokenRightCurlyBrace:
|
||||||
|
p.getToken()
|
||||||
|
break Loop
|
||||||
|
case tokenKey:
|
||||||
|
if !tokenIsComma(previous) && previous != nil {
|
||||||
|
p.raiseError(follow, "comma expected between fields in inline table")
|
||||||
|
}
|
||||||
|
key := p.getToken()
|
||||||
|
p.assume(tokenEqual)
|
||||||
|
value := p.parseRvalue()
|
||||||
|
tree.Set(key.val, value)
|
||||||
|
case tokenComma:
|
||||||
|
if previous == nil {
|
||||||
|
p.raiseError(follow, "inline table cannot start with a comma")
|
||||||
|
}
|
||||||
|
if tokenIsComma(previous) {
|
||||||
|
p.raiseError(follow, "need field between two commas in inline table")
|
||||||
|
}
|
||||||
|
p.getToken()
|
||||||
|
default:
|
||||||
|
p.raiseError(follow, "unexpected token type in inline table: %s", follow.typ.String())
|
||||||
|
}
|
||||||
|
previous = follow
|
||||||
|
}
|
||||||
|
if tokenIsComma(previous) {
|
||||||
|
p.raiseError(previous, "trailing comma at the end of inline table")
|
||||||
|
}
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) parseArray() interface{} {
|
||||||
|
var array []interface{}
|
||||||
|
arrayType := reflect.TypeOf(nil)
|
||||||
|
for {
|
||||||
|
follow := p.peek()
|
||||||
|
if follow == nil || follow.typ == tokenEOF {
|
||||||
|
p.raiseError(follow, "unterminated array")
|
||||||
|
}
|
||||||
|
if follow.typ == tokenRightBracket {
|
||||||
|
p.getToken()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
val := p.parseRvalue()
|
||||||
|
if arrayType == nil {
|
||||||
|
arrayType = reflect.TypeOf(val)
|
||||||
|
}
|
||||||
|
if reflect.TypeOf(val) != arrayType {
|
||||||
|
p.raiseError(follow, "mixed types in array")
|
||||||
|
}
|
||||||
|
array = append(array, val)
|
||||||
|
follow = p.peek()
|
||||||
|
if follow == nil || follow.typ == tokenEOF {
|
||||||
|
p.raiseError(follow, "unterminated array")
|
||||||
|
}
|
||||||
|
if follow.typ != tokenRightBracket && follow.typ != tokenComma {
|
||||||
|
p.raiseError(follow, "missing comma")
|
||||||
|
}
|
||||||
|
if follow.typ == tokenComma {
|
||||||
|
p.getToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// An array of Trees is actually an array of inline
|
||||||
|
// tables, which is a shorthand for a table array. If the
|
||||||
|
// array was not converted from []interface{} to []*Tree,
|
||||||
|
// the two notations would not be equivalent.
|
||||||
|
if arrayType == reflect.TypeOf(newTree()) {
|
||||||
|
tomlArray := make([]*Tree, len(array))
|
||||||
|
for i, v := range array {
|
||||||
|
tomlArray[i] = v.(*Tree)
|
||||||
|
}
|
||||||
|
return tomlArray
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseToml(flow chan token) *Tree {
|
||||||
|
result := newTree()
|
||||||
|
result.position = Position{1, 1}
|
||||||
|
parser := &tomlParser{
|
||||||
|
flow: flow,
|
||||||
|
tree: result,
|
||||||
|
tokensBuffer: make([]token, 0),
|
||||||
|
currentTable: make([]string, 0),
|
||||||
|
seenTableKeys: make([]string, 0),
|
||||||
|
}
|
||||||
|
parser.run()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
numberUnderscoreInvalidRegexp = regexp.MustCompile(`([^\d]_|_[^\d]|_$|^_)`)
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Position support for go-toml
|
||||||
|
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Position of a document element within a TOML document.
|
||||||
|
//
|
||||||
|
// Line and Col are both 1-indexed positions for the element's line number and
|
||||||
|
// column number, respectively. Values of zero or less will cause Invalid(),
|
||||||
|
// to return true.
|
||||||
|
type Position struct {
|
||||||
|
Line int // line within the document
|
||||||
|
Col int // column within the line
|
||||||
|
}
|
||||||
|
|
||||||
|
// String representation of the position.
|
||||||
|
// Displays 1-indexed line and column numbers.
|
||||||
|
func (p Position) String() string {
|
||||||
|
return fmt.Sprintf("(%d, %d)", p.Line, p.Col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid returns whether or not the position is valid (i.e. with negative or
|
||||||
|
// null values)
|
||||||
|
func (p Position) Invalid() bool {
|
||||||
|
return p.Line <= 0 || p.Col <= 0
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Define tokens
|
||||||
|
type tokenType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
eof = -(iota + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tokenError tokenType = iota
|
||||||
|
tokenEOF
|
||||||
|
tokenComment
|
||||||
|
tokenKey
|
||||||
|
tokenString
|
||||||
|
tokenInteger
|
||||||
|
tokenTrue
|
||||||
|
tokenFalse
|
||||||
|
tokenFloat
|
||||||
|
tokenEqual
|
||||||
|
tokenLeftBracket
|
||||||
|
tokenRightBracket
|
||||||
|
tokenLeftCurlyBrace
|
||||||
|
tokenRightCurlyBrace
|
||||||
|
tokenLeftParen
|
||||||
|
tokenRightParen
|
||||||
|
tokenDoubleLeftBracket
|
||||||
|
tokenDoubleRightBracket
|
||||||
|
tokenDate
|
||||||
|
tokenKeyGroup
|
||||||
|
tokenKeyGroupArray
|
||||||
|
tokenComma
|
||||||
|
tokenColon
|
||||||
|
tokenDollar
|
||||||
|
tokenStar
|
||||||
|
tokenQuestion
|
||||||
|
tokenDot
|
||||||
|
tokenDotDot
|
||||||
|
tokenEOL
|
||||||
|
)
|
||||||
|
|
||||||
|
var tokenTypeNames = []string{
|
||||||
|
"Error",
|
||||||
|
"EOF",
|
||||||
|
"Comment",
|
||||||
|
"Key",
|
||||||
|
"String",
|
||||||
|
"Integer",
|
||||||
|
"True",
|
||||||
|
"False",
|
||||||
|
"Float",
|
||||||
|
"=",
|
||||||
|
"[",
|
||||||
|
"]",
|
||||||
|
"{",
|
||||||
|
"}",
|
||||||
|
"(",
|
||||||
|
")",
|
||||||
|
"]]",
|
||||||
|
"[[",
|
||||||
|
"Date",
|
||||||
|
"KeyGroup",
|
||||||
|
"KeyGroupArray",
|
||||||
|
",",
|
||||||
|
":",
|
||||||
|
"$",
|
||||||
|
"*",
|
||||||
|
"?",
|
||||||
|
".",
|
||||||
|
"..",
|
||||||
|
"EOL",
|
||||||
|
}
|
||||||
|
|
||||||
|
type token struct {
|
||||||
|
Position
|
||||||
|
typ tokenType
|
||||||
|
val string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tt tokenType) String() string {
|
||||||
|
idx := int(tt)
|
||||||
|
if idx < len(tokenTypeNames) {
|
||||||
|
return tokenTypeNames[idx]
|
||||||
|
}
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t token) Int() int {
|
||||||
|
if result, err := strconv.Atoi(t.val); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t token) String() string {
|
||||||
|
switch t.typ {
|
||||||
|
case tokenEOF:
|
||||||
|
return "EOF"
|
||||||
|
case tokenError:
|
||||||
|
return t.val
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%q", t.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpace(r rune) bool {
|
||||||
|
return r == ' ' || r == '\t'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAlphanumeric(r rune) bool {
|
||||||
|
return unicode.IsLetter(r) || r == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKeyChar(r rune) bool {
|
||||||
|
// Keys start with the first character that isn't whitespace or [ and end
|
||||||
|
// with the last non-whitespace character before the equals sign. Keys
|
||||||
|
// cannot contain a # character."
|
||||||
|
return !(r == '\r' || r == '\n' || r == eof || r == '=')
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKeyStartChar(r rune) bool {
|
||||||
|
return !(isSpace(r) || r == '\r' || r == '\n' || r == eof || r == '[')
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDigit(r rune) bool {
|
||||||
|
return unicode.IsNumber(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHexDigit(r rune) bool {
|
||||||
|
return isDigit(r) ||
|
||||||
|
(r >= 'a' && r <= 'f') ||
|
||||||
|
(r >= 'A' && r <= 'F')
|
||||||
|
}
|
|
@ -0,0 +1,281 @@
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tomlValue struct {
|
||||||
|
value interface{} // string, int64, uint64, float64, bool, time.Time, [] of any of this list
|
||||||
|
position Position
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tree is the result of the parsing of a TOML file.
|
||||||
|
type Tree struct {
|
||||||
|
values map[string]interface{} // string -> *tomlValue, *Tree, []*Tree
|
||||||
|
position Position
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTree() *Tree {
|
||||||
|
return &Tree{
|
||||||
|
values: make(map[string]interface{}),
|
||||||
|
position: Position{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TreeFromMap initializes a new Tree object using the given map.
|
||||||
|
func TreeFromMap(m map[string]interface{}) (*Tree, error) {
|
||||||
|
result, err := toTree(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result.(*Tree), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position returns the position of the tree.
|
||||||
|
func (t *Tree) Position() Position {
|
||||||
|
return t.position
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has returns a boolean indicating if the given key exists.
|
||||||
|
func (t *Tree) Has(key string) bool {
|
||||||
|
if key == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return t.HasPath(strings.Split(key, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPath returns true if the given path of keys exists, false otherwise.
|
||||||
|
func (t *Tree) HasPath(keys []string) bool {
|
||||||
|
return t.GetPath(keys) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returns the keys of the toplevel tree (does not recurse).
|
||||||
|
func (t *Tree) Keys() []string {
|
||||||
|
keys := make([]string, len(t.values))
|
||||||
|
i := 0
|
||||||
|
for k := range t.values {
|
||||||
|
keys[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value at key in the Tree.
|
||||||
|
// Key is a dot-separated path (e.g. a.b.c).
|
||||||
|
// Returns nil if the path does not exist in the tree.
|
||||||
|
// If keys is of length zero, the current tree is returned.
|
||||||
|
func (t *Tree) Get(key string) interface{} {
|
||||||
|
if key == "" {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
comps, err := parseKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return t.GetPath(comps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPath returns the element in the tree indicated by 'keys'.
|
||||||
|
// If keys is of length zero, the current tree is returned.
|
||||||
|
func (t *Tree) GetPath(keys []string) interface{} {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
subtree := t
|
||||||
|
for _, intermediateKey := range keys[:len(keys)-1] {
|
||||||
|
value, exists := subtree.values[intermediateKey]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch node := value.(type) {
|
||||||
|
case *Tree:
|
||||||
|
subtree = node
|
||||||
|
case []*Tree:
|
||||||
|
// go to most recent element
|
||||||
|
if len(node) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
subtree = node[len(node)-1]
|
||||||
|
default:
|
||||||
|
return nil // cannot navigate through other node types
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// branch based on final node type
|
||||||
|
switch node := subtree.values[keys[len(keys)-1]].(type) {
|
||||||
|
case *tomlValue:
|
||||||
|
return node.value
|
||||||
|
default:
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPosition returns the position of the given key.
|
||||||
|
func (t *Tree) GetPosition(key string) Position {
|
||||||
|
if key == "" {
|
||||||
|
return t.position
|
||||||
|
}
|
||||||
|
return t.GetPositionPath(strings.Split(key, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPositionPath returns the element in the tree indicated by 'keys'.
|
||||||
|
// If keys is of length zero, the current tree is returned.
|
||||||
|
func (t *Tree) GetPositionPath(keys []string) Position {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return t.position
|
||||||
|
}
|
||||||
|
subtree := t
|
||||||
|
for _, intermediateKey := range keys[:len(keys)-1] {
|
||||||
|
value, exists := subtree.values[intermediateKey]
|
||||||
|
if !exists {
|
||||||
|
return Position{0, 0}
|
||||||
|
}
|
||||||
|
switch node := value.(type) {
|
||||||
|
case *Tree:
|
||||||
|
subtree = node
|
||||||
|
case []*Tree:
|
||||||
|
// go to most recent element
|
||||||
|
if len(node) == 0 {
|
||||||
|
return Position{0, 0}
|
||||||
|
}
|
||||||
|
subtree = node[len(node)-1]
|
||||||
|
default:
|
||||||
|
return Position{0, 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// branch based on final node type
|
||||||
|
switch node := subtree.values[keys[len(keys)-1]].(type) {
|
||||||
|
case *tomlValue:
|
||||||
|
return node.position
|
||||||
|
case *Tree:
|
||||||
|
return node.position
|
||||||
|
case []*Tree:
|
||||||
|
// go to most recent element
|
||||||
|
if len(node) == 0 {
|
||||||
|
return Position{0, 0}
|
||||||
|
}
|
||||||
|
return node[len(node)-1].position
|
||||||
|
default:
|
||||||
|
return Position{0, 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefault works like Get but with a default value
|
||||||
|
func (t *Tree) GetDefault(key string, def interface{}) interface{} {
|
||||||
|
val := t.Get(key)
|
||||||
|
if val == nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set an element in the tree.
|
||||||
|
// Key is a dot-separated path (e.g. a.b.c).
|
||||||
|
// Creates all necessary intermediate trees, if needed.
|
||||||
|
func (t *Tree) Set(key string, value interface{}) {
|
||||||
|
t.SetPath(strings.Split(key, "."), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPath sets an element in the tree.
|
||||||
|
// Keys is an array of path elements (e.g. {"a","b","c"}).
|
||||||
|
// Creates all necessary intermediate trees, if needed.
|
||||||
|
func (t *Tree) SetPath(keys []string, value interface{}) {
|
||||||
|
subtree := t
|
||||||
|
for _, intermediateKey := range keys[:len(keys)-1] {
|
||||||
|
nextTree, exists := subtree.values[intermediateKey]
|
||||||
|
if !exists {
|
||||||
|
nextTree = newTree()
|
||||||
|
subtree.values[intermediateKey] = nextTree // add new element here
|
||||||
|
}
|
||||||
|
switch node := nextTree.(type) {
|
||||||
|
case *Tree:
|
||||||
|
subtree = node
|
||||||
|
case []*Tree:
|
||||||
|
// go to most recent element
|
||||||
|
if len(node) == 0 {
|
||||||
|
// create element if it does not exist
|
||||||
|
subtree.values[intermediateKey] = append(node, newTree())
|
||||||
|
}
|
||||||
|
subtree = node[len(node)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var toInsert interface{}
|
||||||
|
|
||||||
|
switch value.(type) {
|
||||||
|
case *Tree:
|
||||||
|
toInsert = value
|
||||||
|
case []*Tree:
|
||||||
|
toInsert = value
|
||||||
|
case *tomlValue:
|
||||||
|
toInsert = value
|
||||||
|
default:
|
||||||
|
toInsert = &tomlValue{value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
subtree.values[keys[len(keys)-1]] = toInsert
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSubTree takes a tree and a key and create the necessary intermediate
|
||||||
|
// subtrees to create a subtree at that point. In-place.
|
||||||
|
//
|
||||||
|
// e.g. passing a.b.c will create (assuming tree is empty) tree[a], tree[a][b]
|
||||||
|
// and tree[a][b][c]
|
||||||
|
//
|
||||||
|
// Returns nil on success, error object on failure
|
||||||
|
func (t *Tree) createSubTree(keys []string, pos Position) error {
|
||||||
|
subtree := t
|
||||||
|
for _, intermediateKey := range keys {
|
||||||
|
nextTree, exists := subtree.values[intermediateKey]
|
||||||
|
if !exists {
|
||||||
|
tree := newTree()
|
||||||
|
tree.position = pos
|
||||||
|
subtree.values[intermediateKey] = tree
|
||||||
|
nextTree = tree
|
||||||
|
}
|
||||||
|
|
||||||
|
switch node := nextTree.(type) {
|
||||||
|
case []*Tree:
|
||||||
|
subtree = node[len(node)-1]
|
||||||
|
case *Tree:
|
||||||
|
subtree = node
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown type for path %s (%s): %T (%#v)",
|
||||||
|
strings.Join(keys, "."), intermediateKey, nextTree, nextTree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadReader creates a Tree from any io.Reader.
|
||||||
|
func LoadReader(reader io.Reader) (tree *Tree, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if _, ok := r.(runtime.Error); ok {
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
err = errors.New(r.(string))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
tree = parseToml(lexToml(reader))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load creates a Tree from a string.
|
||||||
|
func Load(content string) (tree *Tree, err error) {
|
||||||
|
return LoadReader(strings.NewReader(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile creates a Tree from a file.
|
||||||
|
func LoadFile(path string) (tree *Tree, err error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
return LoadReader(file)
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var kindToType = [reflect.String + 1]reflect.Type{
|
||||||
|
reflect.Bool: reflect.TypeOf(true),
|
||||||
|
reflect.String: reflect.TypeOf(""),
|
||||||
|
reflect.Float32: reflect.TypeOf(float64(1)),
|
||||||
|
reflect.Float64: reflect.TypeOf(float64(1)),
|
||||||
|
reflect.Int: reflect.TypeOf(int64(1)),
|
||||||
|
reflect.Int8: reflect.TypeOf(int64(1)),
|
||||||
|
reflect.Int16: reflect.TypeOf(int64(1)),
|
||||||
|
reflect.Int32: reflect.TypeOf(int64(1)),
|
||||||
|
reflect.Int64: reflect.TypeOf(int64(1)),
|
||||||
|
reflect.Uint: reflect.TypeOf(uint64(1)),
|
||||||
|
reflect.Uint8: reflect.TypeOf(uint64(1)),
|
||||||
|
reflect.Uint16: reflect.TypeOf(uint64(1)),
|
||||||
|
reflect.Uint32: reflect.TypeOf(uint64(1)),
|
||||||
|
reflect.Uint64: reflect.TypeOf(uint64(1)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// typeFor returns a reflect.Type for a reflect.Kind, or nil if none is found.
|
||||||
|
// supported values:
|
||||||
|
// string, bool, int64, uint64, float64, time.Time, int, int8, int16, int32, uint, uint8, uint16, uint32, float32
|
||||||
|
func typeFor(k reflect.Kind) reflect.Type {
|
||||||
|
if k > 0 && int(k) < len(kindToType) {
|
||||||
|
return kindToType[k]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func simpleValueCoercion(object interface{}) (interface{}, error) {
|
||||||
|
switch original := object.(type) {
|
||||||
|
case string, bool, int64, uint64, float64, time.Time:
|
||||||
|
return original, nil
|
||||||
|
case int:
|
||||||
|
return int64(original), nil
|
||||||
|
case int8:
|
||||||
|
return int64(original), nil
|
||||||
|
case int16:
|
||||||
|
return int64(original), nil
|
||||||
|
case int32:
|
||||||
|
return int64(original), nil
|
||||||
|
case uint:
|
||||||
|
return uint64(original), nil
|
||||||
|
case uint8:
|
||||||
|
return uint64(original), nil
|
||||||
|
case uint16:
|
||||||
|
return uint64(original), nil
|
||||||
|
case uint32:
|
||||||
|
return uint64(original), nil
|
||||||
|
case float32:
|
||||||
|
return float64(original), nil
|
||||||
|
case fmt.Stringer:
|
||||||
|
return original.String(), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("cannot convert type %T to Tree", object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceToTree(object interface{}) (interface{}, error) {
|
||||||
|
// arrays are a bit tricky, since they can represent either a
|
||||||
|
// collection of simple values, which is represented by one
|
||||||
|
// *tomlValue, or an array of tables, which is represented by an
|
||||||
|
// array of *Tree.
|
||||||
|
|
||||||
|
// holding the assumption that this function is called from toTree only when value.Kind() is Array or Slice
|
||||||
|
value := reflect.ValueOf(object)
|
||||||
|
insideType := value.Type().Elem()
|
||||||
|
length := value.Len()
|
||||||
|
if length > 0 {
|
||||||
|
insideType = reflect.ValueOf(value.Index(0).Interface()).Type()
|
||||||
|
}
|
||||||
|
if insideType.Kind() == reflect.Map {
|
||||||
|
// this is considered as an array of tables
|
||||||
|
tablesArray := make([]*Tree, 0, length)
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
table := value.Index(i)
|
||||||
|
tree, err := toTree(table.Interface())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tablesArray = append(tablesArray, tree.(*Tree))
|
||||||
|
}
|
||||||
|
return tablesArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceType := typeFor(insideType.Kind())
|
||||||
|
if sliceType == nil {
|
||||||
|
sliceType = insideType
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayValue := reflect.MakeSlice(reflect.SliceOf(sliceType), 0, length)
|
||||||
|
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
val := value.Index(i).Interface()
|
||||||
|
simpleValue, err := simpleValueCoercion(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
arrayValue = reflect.Append(arrayValue, reflect.ValueOf(simpleValue))
|
||||||
|
}
|
||||||
|
return &tomlValue{arrayValue.Interface(), Position{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toTree(object interface{}) (interface{}, error) {
|
||||||
|
value := reflect.ValueOf(object)
|
||||||
|
|
||||||
|
if value.Kind() == reflect.Map {
|
||||||
|
values := map[string]interface{}{}
|
||||||
|
keys := value.MapKeys()
|
||||||
|
for _, key := range keys {
|
||||||
|
if key.Kind() != reflect.String {
|
||||||
|
if _, ok := key.Interface().(string); !ok {
|
||||||
|
return nil, fmt.Errorf("map key needs to be a string, not %T (%v)", key.Interface(), key.Kind())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v := value.MapIndex(key)
|
||||||
|
newValue, err := toTree(v.Interface())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
values[key.String()] = newValue
|
||||||
|
}
|
||||||
|
return &Tree{values, Position{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.Kind() == reflect.Array || value.Kind() == reflect.Slice {
|
||||||
|
return sliceToTree(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
simpleValue, err := simpleValueCoercion(object)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tomlValue{simpleValue, Position{}}, nil
|
||||||
|
}
|
|
@ -0,0 +1,224 @@
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encodes a string to a TOML-compliant string value
|
||||||
|
func encodeTomlString(value string) string {
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
for _, rr := range value {
|
||||||
|
switch rr {
|
||||||
|
case '\b':
|
||||||
|
b.WriteString(`\b`)
|
||||||
|
case '\t':
|
||||||
|
b.WriteString(`\t`)
|
||||||
|
case '\n':
|
||||||
|
b.WriteString(`\n`)
|
||||||
|
case '\f':
|
||||||
|
b.WriteString(`\f`)
|
||||||
|
case '\r':
|
||||||
|
b.WriteString(`\r`)
|
||||||
|
case '"':
|
||||||
|
b.WriteString(`\"`)
|
||||||
|
case '\\':
|
||||||
|
b.WriteString(`\\`)
|
||||||
|
default:
|
||||||
|
intRr := uint16(rr)
|
||||||
|
if intRr < 0x001F {
|
||||||
|
b.WriteString(fmt.Sprintf("\\u%0.4X", intRr))
|
||||||
|
} else {
|
||||||
|
b.WriteRune(rr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func tomlValueStringRepresentation(v interface{}) (string, error) {
|
||||||
|
switch value := v.(type) {
|
||||||
|
case uint64:
|
||||||
|
return strconv.FormatUint(value, 10), nil
|
||||||
|
case int64:
|
||||||
|
return strconv.FormatInt(value, 10), nil
|
||||||
|
case float64:
|
||||||
|
// Ensure a round float does contain a decimal point. Otherwise feeding
|
||||||
|
// the output back to the parser would convert to an integer.
|
||||||
|
if math.Trunc(value) == value {
|
||||||
|
return strconv.FormatFloat(value, 'f', 1, 32), nil
|
||||||
|
}
|
||||||
|
return strconv.FormatFloat(value, 'f', -1, 32), nil
|
||||||
|
case string:
|
||||||
|
return "\"" + encodeTomlString(value) + "\"", nil
|
||||||
|
case []byte:
|
||||||
|
b, _ := v.([]byte)
|
||||||
|
return tomlValueStringRepresentation(string(b))
|
||||||
|
case bool:
|
||||||
|
if value {
|
||||||
|
return "true", nil
|
||||||
|
}
|
||||||
|
return "false", nil
|
||||||
|
case time.Time:
|
||||||
|
return value.Format(time.RFC3339), nil
|
||||||
|
case nil:
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := reflect.ValueOf(v)
|
||||||
|
|
||||||
|
if rv.Kind() == reflect.Slice {
|
||||||
|
values := []string{}
|
||||||
|
for i := 0; i < rv.Len(); i++ {
|
||||||
|
item := rv.Index(i).Interface()
|
||||||
|
itemRepr, err := tomlValueStringRepresentation(item)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
values = append(values, itemRepr)
|
||||||
|
}
|
||||||
|
return "[" + strings.Join(values, ",") + "]", nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("unsupported value type %T: %v", v, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64) (int64, error) {
|
||||||
|
simpleValuesKeys := make([]string, 0)
|
||||||
|
complexValuesKeys := make([]string, 0)
|
||||||
|
|
||||||
|
for k := range t.values {
|
||||||
|
v := t.values[k]
|
||||||
|
switch v.(type) {
|
||||||
|
case *Tree, []*Tree:
|
||||||
|
complexValuesKeys = append(complexValuesKeys, k)
|
||||||
|
default:
|
||||||
|
simpleValuesKeys = append(simpleValuesKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(simpleValuesKeys)
|
||||||
|
sort.Strings(complexValuesKeys)
|
||||||
|
|
||||||
|
for _, k := range simpleValuesKeys {
|
||||||
|
v, ok := t.values[k].(*tomlValue)
|
||||||
|
if !ok {
|
||||||
|
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
|
||||||
|
}
|
||||||
|
|
||||||
|
repr, err := tomlValueStringRepresentation(v.value)
|
||||||
|
if err != nil {
|
||||||
|
return bytesCount, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kvRepr := indent + k + " = " + repr + "\n"
|
||||||
|
writtenBytesCount, err := w.Write([]byte(kvRepr))
|
||||||
|
bytesCount += int64(writtenBytesCount)
|
||||||
|
if err != nil {
|
||||||
|
return bytesCount, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range complexValuesKeys {
|
||||||
|
v := t.values[k]
|
||||||
|
|
||||||
|
combinedKey := k
|
||||||
|
if keyspace != "" {
|
||||||
|
combinedKey = keyspace + "." + combinedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
switch node := v.(type) {
|
||||||
|
// node has to be of those two types given how keys are sorted above
|
||||||
|
case *Tree:
|
||||||
|
tableName := "\n" + indent + "[" + combinedKey + "]\n"
|
||||||
|
writtenBytesCount, err := w.Write([]byte(tableName))
|
||||||
|
bytesCount += int64(writtenBytesCount)
|
||||||
|
if err != nil {
|
||||||
|
return bytesCount, err
|
||||||
|
}
|
||||||
|
bytesCount, err = node.writeTo(w, indent+" ", combinedKey, bytesCount)
|
||||||
|
if err != nil {
|
||||||
|
return bytesCount, err
|
||||||
|
}
|
||||||
|
case []*Tree:
|
||||||
|
for _, subTree := range node {
|
||||||
|
tableArrayName := "\n" + indent + "[[" + combinedKey + "]]\n"
|
||||||
|
writtenBytesCount, err := w.Write([]byte(tableArrayName))
|
||||||
|
bytesCount += int64(writtenBytesCount)
|
||||||
|
if err != nil {
|
||||||
|
return bytesCount, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesCount, err = subTree.writeTo(w, indent+" ", combinedKey, bytesCount)
|
||||||
|
if err != nil {
|
||||||
|
return bytesCount, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytesCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTo encode the Tree as Toml and writes it to the writer w.
|
||||||
|
// Returns the number of bytes written in case of success, or an error if anything happened.
|
||||||
|
func (t *Tree) WriteTo(w io.Writer) (int64, error) {
|
||||||
|
return t.writeTo(w, "", "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTomlString generates a human-readable representation of the current tree.
|
||||||
|
// Output spans multiple lines, and is suitable for ingest by a TOML parser.
|
||||||
|
// If the conversion cannot be performed, ToString returns a non-nil error.
|
||||||
|
func (t *Tree) ToTomlString() (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_, err := t.WriteTo(&buf)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String generates a human-readable representation of the current tree.
|
||||||
|
// Alias of ToString. Present to implement the fmt.Stringer interface.
|
||||||
|
func (t *Tree) String() string {
|
||||||
|
result, _ := t.ToTomlString()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMap recursively generates a representation of the tree using Go built-in structures.
|
||||||
|
// The following types are used:
|
||||||
|
//
|
||||||
|
// * bool
|
||||||
|
// * float64
|
||||||
|
// * int64
|
||||||
|
// * string
|
||||||
|
// * uint64
|
||||||
|
// * time.Time
|
||||||
|
// * map[string]interface{} (where interface{} is any of this list)
|
||||||
|
// * []interface{} (where interface{} is any of this list)
|
||||||
|
func (t *Tree) ToMap() map[string]interface{} {
|
||||||
|
result := map[string]interface{}{}
|
||||||
|
|
||||||
|
for k, v := range t.values {
|
||||||
|
switch node := v.(type) {
|
||||||
|
case []*Tree:
|
||||||
|
var array []interface{}
|
||||||
|
for _, item := range node {
|
||||||
|
array = append(array, item.ToMap())
|
||||||
|
}
|
||||||
|
result[k] = array
|
||||||
|
case *Tree:
|
||||||
|
result[k] = node.ToMap()
|
||||||
|
case *tomlValue:
|
||||||
|
result[k] = node.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
|
@ -7,7 +7,7 @@
|
||||||
// and between processes.
|
// and between processes.
|
||||||
//
|
//
|
||||||
// Incoming requests to a server should create a Context, and outgoing calls to
|
// Incoming requests to a server should create a Context, and outgoing calls to
|
||||||
// servers should accept a Context. The chain of function calls between must
|
// servers should accept a Context. The chain of function calls between must
|
||||||
// propagate the Context, optionally replacing it with a modified copy created
|
// propagate the Context, optionally replacing it with a modified copy created
|
||||||
// using WithDeadline, WithTimeout, WithCancel, or WithValue.
|
// using WithDeadline, WithTimeout, WithCancel, or WithValue.
|
||||||
//
|
//
|
||||||
|
@ -16,14 +16,14 @@
|
||||||
// propagation:
|
// propagation:
|
||||||
//
|
//
|
||||||
// Do not store Contexts inside a struct type; instead, pass a Context
|
// Do not store Contexts inside a struct type; instead, pass a Context
|
||||||
// explicitly to each function that needs it. The Context should be the first
|
// explicitly to each function that needs it. The Context should be the first
|
||||||
// parameter, typically named ctx:
|
// parameter, typically named ctx:
|
||||||
//
|
//
|
||||||
// func DoSomething(ctx context.Context, arg Arg) error {
|
// func DoSomething(ctx context.Context, arg Arg) error {
|
||||||
// // ... use ctx ...
|
// // ... use ctx ...
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
|
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
|
||||||
// if you are unsure about which Context to use.
|
// if you are unsure about which Context to use.
|
||||||
//
|
//
|
||||||
// Use context Values only for request-scoped data that transits processes and
|
// Use context Values only for request-scoped data that transits processes and
|
||||||
|
@ -44,13 +44,13 @@ import "time"
|
||||||
// Context's methods may be called by multiple goroutines simultaneously.
|
// Context's methods may be called by multiple goroutines simultaneously.
|
||||||
type Context interface {
|
type Context interface {
|
||||||
// Deadline returns the time when work done on behalf of this context
|
// Deadline returns the time when work done on behalf of this context
|
||||||
// should be canceled. Deadline returns ok==false when no deadline is
|
// should be canceled. Deadline returns ok==false when no deadline is
|
||||||
// set. Successive calls to Deadline return the same results.
|
// set. Successive calls to Deadline return the same results.
|
||||||
Deadline() (deadline time.Time, ok bool)
|
Deadline() (deadline time.Time, ok bool)
|
||||||
|
|
||||||
// Done returns a channel that's closed when work done on behalf of this
|
// Done returns a channel that's closed when work done on behalf of this
|
||||||
// context should be canceled. Done may return nil if this context can
|
// context should be canceled. Done may return nil if this context can
|
||||||
// never be canceled. Successive calls to Done return the same value.
|
// never be canceled. Successive calls to Done return the same value.
|
||||||
//
|
//
|
||||||
// WithCancel arranges for Done to be closed when cancel is called;
|
// WithCancel arranges for Done to be closed when cancel is called;
|
||||||
// WithDeadline arranges for Done to be closed when the deadline
|
// WithDeadline arranges for Done to be closed when the deadline
|
||||||
|
@ -79,24 +79,24 @@ type Context interface {
|
||||||
// a Done channel for cancelation.
|
// a Done channel for cancelation.
|
||||||
Done() <-chan struct{}
|
Done() <-chan struct{}
|
||||||
|
|
||||||
// Err returns a non-nil error value after Done is closed. Err returns
|
// Err returns a non-nil error value after Done is closed. Err returns
|
||||||
// Canceled if the context was canceled or DeadlineExceeded if the
|
// Canceled if the context was canceled or DeadlineExceeded if the
|
||||||
// context's deadline passed. No other values for Err are defined.
|
// context's deadline passed. No other values for Err are defined.
|
||||||
// After Done is closed, successive calls to Err return the same value.
|
// After Done is closed, successive calls to Err return the same value.
|
||||||
Err() error
|
Err() error
|
||||||
|
|
||||||
// Value returns the value associated with this context for key, or nil
|
// Value returns the value associated with this context for key, or nil
|
||||||
// if no value is associated with key. Successive calls to Value with
|
// if no value is associated with key. Successive calls to Value with
|
||||||
// the same key returns the same result.
|
// the same key returns the same result.
|
||||||
//
|
//
|
||||||
// Use context values only for request-scoped data that transits
|
// Use context values only for request-scoped data that transits
|
||||||
// processes and API boundaries, not for passing optional parameters to
|
// processes and API boundaries, not for passing optional parameters to
|
||||||
// functions.
|
// functions.
|
||||||
//
|
//
|
||||||
// A key identifies a specific value in a Context. Functions that wish
|
// A key identifies a specific value in a Context. Functions that wish
|
||||||
// to store values in Context typically allocate a key in a global
|
// to store values in Context typically allocate a key in a global
|
||||||
// variable then use that key as the argument to context.WithValue and
|
// variable then use that key as the argument to context.WithValue and
|
||||||
// Context.Value. A key can be any type that supports equality;
|
// Context.Value. A key can be any type that supports equality;
|
||||||
// packages should define keys as an unexported type to avoid
|
// packages should define keys as an unexported type to avoid
|
||||||
// collisions.
|
// collisions.
|
||||||
//
|
//
|
||||||
|
@ -115,7 +115,7 @@ type Context interface {
|
||||||
// // This prevents collisions with keys defined in other packages.
|
// // This prevents collisions with keys defined in other packages.
|
||||||
// type key int
|
// type key int
|
||||||
//
|
//
|
||||||
// // userKey is the key for user.User values in Contexts. It is
|
// // userKey is the key for user.User values in Contexts. It is
|
||||||
// // unexported; clients use user.NewContext and user.FromContext
|
// // unexported; clients use user.NewContext and user.FromContext
|
||||||
// // instead of using this key directly.
|
// // instead of using this key directly.
|
||||||
// var userKey key = 0
|
// var userKey key = 0
|
||||||
|
@ -134,14 +134,14 @@ type Context interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Background returns a non-nil, empty Context. It is never canceled, has no
|
// Background returns a non-nil, empty Context. It is never canceled, has no
|
||||||
// values, and has no deadline. It is typically used by the main function,
|
// values, and has no deadline. It is typically used by the main function,
|
||||||
// initialization, and tests, and as the top-level Context for incoming
|
// initialization, and tests, and as the top-level Context for incoming
|
||||||
// requests.
|
// requests.
|
||||||
func Background() Context {
|
func Background() Context {
|
||||||
return background
|
return background
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO returns a non-nil, empty Context. Code should use context.TODO when
|
// TODO returns a non-nil, empty Context. Code should use context.TODO when
|
||||||
// it's unclear which Context to use or it is not yet available (because the
|
// it's unclear which Context to use or it is not yet available (because the
|
||||||
// surrounding function has not yet been extended to accept a Context
|
// surrounding function has not yet been extended to accept a Context
|
||||||
// parameter). TODO is recognized by static analysis tools that determine
|
// parameter). TODO is recognized by static analysis tools that determine
|
||||||
|
|
|
@ -35,8 +35,8 @@ func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
||||||
// to be no later than d. If the parent's deadline is already earlier than d,
|
// to be no later than d. If the parent's deadline is already earlier than d,
|
||||||
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
||||||
// context's Done channel is closed when the deadline expires, when the returned
|
// context's Done channel is closed when the deadline expires, when the returned
|
||||||
// cancel function is called, or when the parent context's Done channel is
|
// cancel function is called, or when the parent context's Done channel is
|
||||||
// closed, whichever happens first.
|
// closed, whichever happens first.
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
|
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
|
||||||
// struct{}, since vars of this type must have distinct addresses.
|
// struct{}, since vars of this type must have distinct addresses.
|
||||||
type emptyCtx int
|
type emptyCtx int
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ func propagateCancel(parent Context, child canceler) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parentCancelCtx follows a chain of parent references until it finds a
|
// parentCancelCtx follows a chain of parent references until it finds a
|
||||||
// *cancelCtx. This function understands how each of the concrete types in this
|
// *cancelCtx. This function understands how each of the concrete types in this
|
||||||
// package represents its parent.
|
// package represents its parent.
|
||||||
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
|
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
|
||||||
for {
|
for {
|
||||||
|
@ -134,14 +134,14 @@ func removeChild(parent Context, child canceler) {
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// A canceler is a context type that can be canceled directly. The
|
// A canceler is a context type that can be canceled directly. The
|
||||||
// implementations are *cancelCtx and *timerCtx.
|
// implementations are *cancelCtx and *timerCtx.
|
||||||
type canceler interface {
|
type canceler interface {
|
||||||
cancel(removeFromParent bool, err error)
|
cancel(removeFromParent bool, err error)
|
||||||
Done() <-chan struct{}
|
Done() <-chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A cancelCtx can be canceled. When canceled, it also cancels any children
|
// A cancelCtx can be canceled. When canceled, it also cancels any children
|
||||||
// that implement canceler.
|
// that implement canceler.
|
||||||
type cancelCtx struct {
|
type cancelCtx struct {
|
||||||
Context
|
Context
|
||||||
|
@ -193,8 +193,8 @@ func (c *cancelCtx) cancel(removeFromParent bool, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
||||||
// to be no later than d. If the parent's deadline is already earlier than d,
|
// to be no later than d. If the parent's deadline is already earlier than d,
|
||||||
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
||||||
// context's Done channel is closed when the deadline expires, when the returned
|
// context's Done channel is closed when the deadline expires, when the returned
|
||||||
// cancel function is called, or when the parent context's Done channel is
|
// cancel function is called, or when the parent context's Done channel is
|
||||||
// closed, whichever happens first.
|
// closed, whichever happens first.
|
||||||
|
@ -226,8 +226,8 @@ func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
|
||||||
return c, func() { c.cancel(true, Canceled) }
|
return c, func() { c.cancel(true, Canceled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
|
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
|
||||||
// implement Done and Err. It implements cancel by stopping its timer then
|
// implement Done and Err. It implements cancel by stopping its timer then
|
||||||
// delegating to cancelCtx.cancel.
|
// delegating to cancelCtx.cancel.
|
||||||
type timerCtx struct {
|
type timerCtx struct {
|
||||||
*cancelCtx
|
*cancelCtx
|
||||||
|
@ -281,7 +281,7 @@ func WithValue(parent Context, key interface{}, val interface{}) Context {
|
||||||
return &valueCtx{parent, key, val}
|
return &valueCtx{parent, key, val}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A valueCtx carries a key-value pair. It implements Value for that key and
|
// A valueCtx carries a key-value pair. It implements Value for that key and
|
||||||
// delegates all other calls to the embedded Context.
|
// delegates all other calls to the embedded Context.
|
||||||
type valueCtx struct {
|
type valueCtx struct {
|
||||||
Context
|
Context
|
||||||
|
|
|
@ -7,7 +7,8 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"fmt"
|
||||||
|
"github.com/pelletier/go-toml"
|
||||||
"github.com/siddontang/go/ioutil2"
|
"github.com/siddontang/go/ioutil2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -86,11 +87,16 @@ type SnapshotConfig struct {
|
||||||
MaxNum int `toml:"max_num"`
|
MaxNum int `toml:"max_num"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthMethod func(c *Config, password string) bool
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
m sync.RWMutex `toml:"-"`
|
m sync.RWMutex `toml:"-"`
|
||||||
|
|
||||||
AuthPassword string `toml:"auth_password"`
|
AuthPassword string `toml:"auth_password"`
|
||||||
|
|
||||||
|
//AuthMethod custom authentication method
|
||||||
|
AuthMethod AuthMethod `toml:"-"`
|
||||||
|
|
||||||
FileName string `toml:"-"`
|
FileName string `toml:"-"`
|
||||||
|
|
||||||
// Addr can be empty to assign a local address dynamically
|
// Addr can be empty to assign a local address dynamically
|
||||||
|
@ -149,9 +155,8 @@ func NewConfigWithFile(fileName string) (*Config, error) {
|
||||||
func NewConfigWithData(data []byte) (*Config, error) {
|
func NewConfigWithData(data []byte) (*Config, error) {
|
||||||
cfg := NewConfigDefault()
|
cfg := NewConfigDefault()
|
||||||
|
|
||||||
_, err := toml.Decode(string(data), cfg)
|
if err := toml.Unmarshal(data, cfg); err != nil {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("newConfigwithData: unmarashal: %s", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.adjust()
|
cfg.adjust()
|
||||||
|
@ -253,9 +258,15 @@ func (cfg *RocksDBConfig) adjust() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) Dump(w io.Writer) error {
|
func (cfg *Config) Dump(w io.Writer) error {
|
||||||
e := toml.NewEncoder(w)
|
data, err := toml.Marshal(*cfg)
|
||||||
e.Indent = ""
|
if err != nil {
|
||||||
return e.Encode(cfg)
|
return err
|
||||||
|
}
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) DumpFile(fileName string) error {
|
func (cfg *Config) DumpFile(fileName string) error {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
hash: 2375ebb56a7d505734bcecabb6a3ab5f4eaaf885d120ba2fb75c2c18c5fef39e
|
hash: 83229e7ecea008add55b2547fb28a23d247a4740fc30b4659f58bf5d495e3584
|
||||||
updated: 2017-04-14T17:19:55.748440844-05:00
|
updated: 2017-06-08T13:54:52.259026852+02:00
|
||||||
imports:
|
imports:
|
||||||
- name: github.com/BurntSushi/toml
|
|
||||||
version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f
|
|
||||||
- name: github.com/cupcake/rdb
|
- name: github.com/cupcake/rdb
|
||||||
version: 43ba34106c765f2111c0dc7b74cdf8ee437411e0
|
version: 43ba34106c765f2111c0dc7b74cdf8ee437411e0
|
||||||
subpackages:
|
subpackages:
|
||||||
|
@ -14,6 +12,10 @@ imports:
|
||||||
version: dc4743023d0c166c1b844da8fc688e57ec65fe0b
|
version: dc4743023d0c166c1b844da8fc688e57ec65fe0b
|
||||||
- name: github.com/golang/snappy
|
- name: github.com/golang/snappy
|
||||||
version: 553a641470496b2327abcac10b36396bd98e45c9
|
version: 553a641470496b2327abcac10b36396bd98e45c9
|
||||||
|
- name: github.com/pelletier/go-buffruneio
|
||||||
|
version: c37440a7cf42ac63b919c752ca73a85067e05992
|
||||||
|
- name: github.com/pelletier/go-toml
|
||||||
|
version: fe7536c3dee2596cdd23ee9976a17c22bdaae286
|
||||||
- name: github.com/peterh/liner
|
- name: github.com/peterh/liner
|
||||||
version: bf27d3ba8e1d9899d45a457ffac16c953eb2d647
|
version: bf27d3ba8e1d9899d45a457ffac16c953eb2d647
|
||||||
- name: github.com/siddontang/go
|
- name: github.com/siddontang/go
|
||||||
|
@ -57,7 +59,7 @@ imports:
|
||||||
- parse
|
- parse
|
||||||
- pm
|
- pm
|
||||||
- name: golang.org/x/net
|
- name: golang.org/x/net
|
||||||
version: 4971afdc2f162e82d185353533d3cf16188a9f4e
|
version: 59a0b19b5533c7977ddeb86b017bf507ed407b12
|
||||||
subpackages:
|
subpackages:
|
||||||
- context
|
- context
|
||||||
testImports: []
|
testImports: []
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package: github.com/siddontang/ledisdb
|
package: github.com/siddontang/ledisdb
|
||||||
import:
|
import:
|
||||||
- package: github.com/BurntSushi/toml
|
- package: github.com/pelletier/go-toml
|
||||||
version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f
|
version: fe7536c3dee2596cdd23ee9976a17c22bdaae286
|
||||||
- package: github.com/edsrzf/mmap-go
|
- package: github.com/edsrzf/mmap-go
|
||||||
version: 935e0e8a636ca4ba70b713f3e38a19e1b77739e8
|
version: 935e0e8a636ca4ba70b713f3e38a19e1b77739e8
|
||||||
- package: github.com/peterh/liner
|
- package: github.com/peterh/liner
|
||||||
|
|
|
@ -70,7 +70,7 @@ func (c *client) close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) authEnabled() bool {
|
func (c *client) authEnabled() bool {
|
||||||
return len(c.app.cfg.AuthPassword) > 0
|
return len(c.app.cfg.AuthPassword) > 0 || c.app.cfg.AuthMethod != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) perform() {
|
func (c *client) perform() {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/siddontang/go/hack"
|
"github.com/siddontang/go/hack"
|
||||||
"github.com/siddontang/go/num"
|
"github.com/siddontang/go/num"
|
||||||
|
|
||||||
|
"github.com/siddontang/ledisdb/config"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -14,12 +15,21 @@ func pingCommand(c *client) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultAuth(c *config.Config, password string) bool {
|
||||||
|
return c.AuthPassword == password
|
||||||
|
}
|
||||||
|
|
||||||
func authCommand(c *client) error {
|
func authCommand(c *client) error {
|
||||||
if len(c.args) != 1 {
|
if len(c.args) != 1 {
|
||||||
return ErrCmdParams
|
return ErrCmdParams
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.app.cfg.AuthPassword == string(c.args[0]) {
|
method := defaultAuth
|
||||||
|
if c.app.cfg.AuthMethod != nil {
|
||||||
|
method = c.app.cfg.AuthMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
if method(c.app.cfg, string(c.args[0])) {
|
||||||
c.isAuthed = true
|
c.isAuthed = true
|
||||||
c.resp.writeStatus(OK)
|
c.resp.writeStatus(OK)
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Reference in New Issue