541 lines
11 KiB
Go
541 lines
11 KiB
Go
|
package toml
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"reflect"
|
||
|
"testing"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
func init() {
|
||
|
log.SetFlags(0)
|
||
|
}
|
||
|
|
||
|
func TestDecodeSimple(t *testing.T) {
|
||
|
var testSimple = `
|
||
|
age = 250
|
||
|
andrew = "gallant"
|
||
|
kait = "brady"
|
||
|
now = 1987-07-05T05:45:00Z
|
||
|
yesOrNo = true
|
||
|
pi = 3.14
|
||
|
colors = [
|
||
|
["red", "green", "blue"],
|
||
|
["cyan", "magenta", "yellow", "black"],
|
||
|
]
|
||
|
|
||
|
[My.Cats]
|
||
|
plato = "cat 1"
|
||
|
cauchy = "cat 2"
|
||
|
`
|
||
|
|
||
|
type cats struct {
|
||
|
Plato string
|
||
|
Cauchy string
|
||
|
}
|
||
|
type simple struct {
|
||
|
Age int
|
||
|
Colors [][]string
|
||
|
Pi float64
|
||
|
YesOrNo bool
|
||
|
Now time.Time
|
||
|
Andrew string
|
||
|
Kait string
|
||
|
My map[string]cats
|
||
|
}
|
||
|
|
||
|
var val simple
|
||
|
_, err := Decode(testSimple, &val)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
now, err := time.Parse("2006-01-02T15:04:05", "1987-07-05T05:45:00")
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
var answer = simple{
|
||
|
Age: 250,
|
||
|
Andrew: "gallant",
|
||
|
Kait: "brady",
|
||
|
Now: now,
|
||
|
YesOrNo: true,
|
||
|
Pi: 3.14,
|
||
|
Colors: [][]string{
|
||
|
{"red", "green", "blue"},
|
||
|
{"cyan", "magenta", "yellow", "black"},
|
||
|
},
|
||
|
My: map[string]cats{
|
||
|
"Cats": cats{Plato: "cat 1", Cauchy: "cat 2"},
|
||
|
},
|
||
|
}
|
||
|
if !reflect.DeepEqual(val, answer) {
|
||
|
t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n",
|
||
|
answer, val)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestDecodeEmbedded(t *testing.T) {
|
||
|
type Dog struct{ Name string }
|
||
|
type Age int
|
||
|
|
||
|
tests := map[string]struct {
|
||
|
input string
|
||
|
decodeInto interface{}
|
||
|
wantDecoded interface{}
|
||
|
}{
|
||
|
"embedded struct": {
|
||
|
input: `Name = "milton"`,
|
||
|
decodeInto: &struct{ Dog }{},
|
||
|
wantDecoded: &struct{ Dog }{Dog{"milton"}},
|
||
|
},
|
||
|
"embedded non-nil pointer to struct": {
|
||
|
input: `Name = "milton"`,
|
||
|
decodeInto: &struct{ *Dog }{},
|
||
|
wantDecoded: &struct{ *Dog }{&Dog{"milton"}},
|
||
|
},
|
||
|
"embedded nil pointer to struct": {
|
||
|
input: ``,
|
||
|
decodeInto: &struct{ *Dog }{},
|
||
|
wantDecoded: &struct{ *Dog }{nil},
|
||
|
},
|
||
|
"embedded int": {
|
||
|
input: `Age = -5`,
|
||
|
decodeInto: &struct{ Age }{},
|
||
|
wantDecoded: &struct{ Age }{-5},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for label, test := range tests {
|
||
|
_, err := Decode(test.input, test.decodeInto)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
if !reflect.DeepEqual(test.wantDecoded, test.decodeInto) {
|
||
|
t.Errorf("%s: want decoded == %+v, got %+v",
|
||
|
label, test.wantDecoded, test.decodeInto)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestTableArrays(t *testing.T) {
|
||
|
var tomlTableArrays = `
|
||
|
[[albums]]
|
||
|
name = "Born to Run"
|
||
|
|
||
|
[[albums.songs]]
|
||
|
name = "Jungleland"
|
||
|
|
||
|
[[albums.songs]]
|
||
|
name = "Meeting Across the River"
|
||
|
|
||
|
[[albums]]
|
||
|
name = "Born in the USA"
|
||
|
|
||
|
[[albums.songs]]
|
||
|
name = "Glory Days"
|
||
|
|
||
|
[[albums.songs]]
|
||
|
name = "Dancing in the Dark"
|
||
|
`
|
||
|
|
||
|
type Song struct {
|
||
|
Name string
|
||
|
}
|
||
|
|
||
|
type Album struct {
|
||
|
Name string
|
||
|
Songs []Song
|
||
|
}
|
||
|
|
||
|
type Music struct {
|
||
|
Albums []Album
|
||
|
}
|
||
|
|
||
|
expected := Music{[]Album{
|
||
|
{"Born to Run", []Song{{"Jungleland"}, {"Meeting Across the River"}}},
|
||
|
{"Born in the USA", []Song{{"Glory Days"}, {"Dancing in the Dark"}}},
|
||
|
}}
|
||
|
var got Music
|
||
|
if _, err := Decode(tomlTableArrays, &got); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
if !reflect.DeepEqual(expected, got) {
|
||
|
t.Fatalf("\n%#v\n!=\n%#v\n", expected, got)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Case insensitive matching tests.
|
||
|
// A bit more comprehensive than needed given the current implementation,
|
||
|
// but implementations change.
|
||
|
// Probably still missing demonstrations of some ugly corner cases regarding
|
||
|
// case insensitive matching and multiple fields.
|
||
|
func TestCase(t *testing.T) {
|
||
|
var caseToml = `
|
||
|
tOpString = "string"
|
||
|
tOpInt = 1
|
||
|
tOpFloat = 1.1
|
||
|
tOpBool = true
|
||
|
tOpdate = 2006-01-02T15:04:05Z
|
||
|
tOparray = [ "array" ]
|
||
|
Match = "i should be in Match only"
|
||
|
MatcH = "i should be in MatcH only"
|
||
|
once = "just once"
|
||
|
[nEst.eD]
|
||
|
nEstedString = "another string"
|
||
|
`
|
||
|
|
||
|
type InsensitiveEd struct {
|
||
|
NestedString string
|
||
|
}
|
||
|
|
||
|
type InsensitiveNest struct {
|
||
|
Ed InsensitiveEd
|
||
|
}
|
||
|
|
||
|
type Insensitive struct {
|
||
|
TopString string
|
||
|
TopInt int
|
||
|
TopFloat float64
|
||
|
TopBool bool
|
||
|
TopDate time.Time
|
||
|
TopArray []string
|
||
|
Match string
|
||
|
MatcH string
|
||
|
Once string
|
||
|
OncE string
|
||
|
Nest InsensitiveNest
|
||
|
}
|
||
|
|
||
|
tme, err := time.Parse(time.RFC3339, time.RFC3339[:len(time.RFC3339)-5])
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
expected := Insensitive{
|
||
|
TopString: "string",
|
||
|
TopInt: 1,
|
||
|
TopFloat: 1.1,
|
||
|
TopBool: true,
|
||
|
TopDate: tme,
|
||
|
TopArray: []string{"array"},
|
||
|
MatcH: "i should be in MatcH only",
|
||
|
Match: "i should be in Match only",
|
||
|
Once: "just once",
|
||
|
OncE: "",
|
||
|
Nest: InsensitiveNest{
|
||
|
Ed: InsensitiveEd{NestedString: "another string"},
|
||
|
},
|
||
|
}
|
||
|
var got Insensitive
|
||
|
if _, err := Decode(caseToml, &got); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
if !reflect.DeepEqual(expected, got) {
|
||
|
t.Fatalf("\n%#v\n!=\n%#v\n", expected, got)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestPointers(t *testing.T) {
|
||
|
type Object struct {
|
||
|
Type string
|
||
|
Description string
|
||
|
}
|
||
|
|
||
|
type Dict struct {
|
||
|
NamedObject map[string]*Object
|
||
|
BaseObject *Object
|
||
|
Strptr *string
|
||
|
Strptrs []*string
|
||
|
}
|
||
|
s1, s2, s3 := "blah", "abc", "def"
|
||
|
expected := &Dict{
|
||
|
Strptr: &s1,
|
||
|
Strptrs: []*string{&s2, &s3},
|
||
|
NamedObject: map[string]*Object{
|
||
|
"foo": {"FOO", "fooooo!!!"},
|
||
|
"bar": {"BAR", "ba-ba-ba-ba-barrrr!!!"},
|
||
|
},
|
||
|
BaseObject: &Object{"BASE", "da base"},
|
||
|
}
|
||
|
|
||
|
ex1 := `
|
||
|
Strptr = "blah"
|
||
|
Strptrs = ["abc", "def"]
|
||
|
|
||
|
[NamedObject.foo]
|
||
|
Type = "FOO"
|
||
|
Description = "fooooo!!!"
|
||
|
|
||
|
[NamedObject.bar]
|
||
|
Type = "BAR"
|
||
|
Description = "ba-ba-ba-ba-barrrr!!!"
|
||
|
|
||
|
[BaseObject]
|
||
|
Type = "BASE"
|
||
|
Description = "da base"
|
||
|
`
|
||
|
dict := new(Dict)
|
||
|
_, err := Decode(ex1, dict)
|
||
|
if err != nil {
|
||
|
t.Errorf("Decode error: %v", err)
|
||
|
}
|
||
|
if !reflect.DeepEqual(expected, dict) {
|
||
|
t.Fatalf("\n%#v\n!=\n%#v\n", expected, dict)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type sphere struct {
|
||
|
Center [3]float64
|
||
|
Radius float64
|
||
|
}
|
||
|
|
||
|
func TestDecodeSimpleArray(t *testing.T) {
|
||
|
var s1 sphere
|
||
|
if _, err := Decode(`center = [0.0, 1.5, 0.0]`, &s1); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestDecodeArrayWrongSize(t *testing.T) {
|
||
|
var s1 sphere
|
||
|
if _, err := Decode(`center = [0.1, 2.3]`, &s1); err == nil {
|
||
|
t.Fatal("Expected array type mismatch error")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestDecodeLargeIntoSmallInt(t *testing.T) {
|
||
|
type table struct {
|
||
|
Value int8
|
||
|
}
|
||
|
var tab table
|
||
|
if _, err := Decode(`value = 500`, &tab); err == nil {
|
||
|
t.Fatal("Expected integer out-of-bounds error.")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestDecodeSizedInts(t *testing.T) {
|
||
|
type table struct {
|
||
|
U8 uint8
|
||
|
U16 uint16
|
||
|
U32 uint32
|
||
|
U64 uint64
|
||
|
U uint
|
||
|
I8 int8
|
||
|
I16 int16
|
||
|
I32 int32
|
||
|
I64 int64
|
||
|
I int
|
||
|
}
|
||
|
answer := table{1, 1, 1, 1, 1, -1, -1, -1, -1, -1}
|
||
|
toml := `
|
||
|
u8 = 1
|
||
|
u16 = 1
|
||
|
u32 = 1
|
||
|
u64 = 1
|
||
|
u = 1
|
||
|
i8 = -1
|
||
|
i16 = -1
|
||
|
i32 = -1
|
||
|
i64 = -1
|
||
|
i = -1
|
||
|
`
|
||
|
var tab table
|
||
|
if _, err := Decode(toml, &tab); err != nil {
|
||
|
t.Fatal(err.Error())
|
||
|
}
|
||
|
if answer != tab {
|
||
|
t.Fatalf("Expected %#v but got %#v", answer, tab)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func ExampleMetaData_PrimitiveDecode() {
|
||
|
var md MetaData
|
||
|
var err error
|
||
|
|
||
|
var tomlBlob = `
|
||
|
ranking = ["Springsteen", "J Geils"]
|
||
|
|
||
|
[bands.Springsteen]
|
||
|
started = 1973
|
||
|
albums = ["Greetings", "WIESS", "Born to Run", "Darkness"]
|
||
|
|
||
|
[bands.J Geils]
|
||
|
started = 1970
|
||
|
albums = ["The J. Geils Band", "Full House", "Blow Your Face Out"]
|
||
|
`
|
||
|
|
||
|
type band struct {
|
||
|
Started int
|
||
|
Albums []string
|
||
|
}
|
||
|
type classics struct {
|
||
|
Ranking []string
|
||
|
Bands map[string]Primitive
|
||
|
}
|
||
|
|
||
|
// Do the initial decode. Reflection is delayed on Primitive values.
|
||
|
var music classics
|
||
|
if md, err = Decode(tomlBlob, &music); err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// MetaData still includes information on Primitive values.
|
||
|
fmt.Printf("Is `bands.Springsteen` defined? %v\n",
|
||
|
md.IsDefined("bands", "Springsteen"))
|
||
|
|
||
|
// Decode primitive data into Go values.
|
||
|
for _, artist := range music.Ranking {
|
||
|
// A band is a primitive value, so we need to decode it to get a
|
||
|
// real `band` value.
|
||
|
primValue := music.Bands[artist]
|
||
|
|
||
|
var aBand band
|
||
|
if err = md.PrimitiveDecode(primValue, &aBand); err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
fmt.Printf("%s started in %d.\n", artist, aBand.Started)
|
||
|
}
|
||
|
// Check to see if there were any fields left undecoded.
|
||
|
// Note that this won't be empty before decoding the Primitive value!
|
||
|
fmt.Printf("Undecoded: %q\n", md.Undecoded())
|
||
|
|
||
|
// Output:
|
||
|
// Is `bands.Springsteen` defined? true
|
||
|
// Springsteen started in 1973.
|
||
|
// J Geils started in 1970.
|
||
|
// Undecoded: []
|
||
|
}
|
||
|
|
||
|
func ExampleDecode() {
|
||
|
var tomlBlob = `
|
||
|
# Some comments.
|
||
|
[alpha]
|
||
|
ip = "10.0.0.1"
|
||
|
|
||
|
[alpha.config]
|
||
|
Ports = [ 8001, 8002 ]
|
||
|
Location = "Toronto"
|
||
|
Created = 1987-07-05T05:45:00Z
|
||
|
|
||
|
[beta]
|
||
|
ip = "10.0.0.2"
|
||
|
|
||
|
[beta.config]
|
||
|
Ports = [ 9001, 9002 ]
|
||
|
Location = "New Jersey"
|
||
|
Created = 1887-01-05T05:55:00Z
|
||
|
`
|
||
|
|
||
|
type serverConfig struct {
|
||
|
Ports []int
|
||
|
Location string
|
||
|
Created time.Time
|
||
|
}
|
||
|
|
||
|
type server struct {
|
||
|
IP string `toml:"ip"`
|
||
|
Config serverConfig `toml:"config"`
|
||
|
}
|
||
|
|
||
|
type servers map[string]server
|
||
|
|
||
|
var config servers
|
||
|
if _, err := Decode(tomlBlob, &config); err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
for _, name := range []string{"alpha", "beta"} {
|
||
|
s := config[name]
|
||
|
fmt.Printf("Server: %s (ip: %s) in %s created on %s\n",
|
||
|
name, s.IP, s.Config.Location,
|
||
|
s.Config.Created.Format("2006-01-02"))
|
||
|
fmt.Printf("Ports: %v\n", s.Config.Ports)
|
||
|
}
|
||
|
|
||
|
// Output:
|
||
|
// Server: alpha (ip: 10.0.0.1) in Toronto created on 1987-07-05
|
||
|
// Ports: [8001 8002]
|
||
|
// Server: beta (ip: 10.0.0.2) in New Jersey created on 1887-01-05
|
||
|
// Ports: [9001 9002]
|
||
|
}
|
||
|
|
||
|
type duration struct {
|
||
|
time.Duration
|
||
|
}
|
||
|
|
||
|
func (d *duration) UnmarshalText(text []byte) error {
|
||
|
var err error
|
||
|
d.Duration, err = time.ParseDuration(string(text))
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Example Unmarshaler shows how to decode TOML strings into your own
|
||
|
// custom data type.
|
||
|
func Example_unmarshaler() {
|
||
|
blob := `
|
||
|
[[song]]
|
||
|
name = "Thunder Road"
|
||
|
duration = "4m49s"
|
||
|
|
||
|
[[song]]
|
||
|
name = "Stairway to Heaven"
|
||
|
duration = "8m03s"
|
||
|
`
|
||
|
type song struct {
|
||
|
Name string
|
||
|
Duration duration
|
||
|
}
|
||
|
type songs struct {
|
||
|
Song []song
|
||
|
}
|
||
|
var favorites songs
|
||
|
if _, err := Decode(blob, &favorites); err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Code to implement the TextUnmarshaler interface for `duration`:
|
||
|
//
|
||
|
// type duration struct {
|
||
|
// time.Duration
|
||
|
// }
|
||
|
//
|
||
|
// func (d *duration) UnmarshalText(text []byte) error {
|
||
|
// var err error
|
||
|
// d.Duration, err = time.ParseDuration(string(text))
|
||
|
// return err
|
||
|
// }
|
||
|
|
||
|
for _, s := range favorites.Song {
|
||
|
fmt.Printf("%s (%s)\n", s.Name, s.Duration)
|
||
|
}
|
||
|
// Output:
|
||
|
// Thunder Road (4m49s)
|
||
|
// Stairway to Heaven (8m3s)
|
||
|
}
|
||
|
|
||
|
// Example StrictDecoding shows how to detect whether there are keys in the
|
||
|
// TOML document that weren't decoded into the value given. This is useful
|
||
|
// for returning an error to the user if they've included extraneous fields
|
||
|
// in their configuration.
|
||
|
func Example_strictDecoding() {
|
||
|
var blob = `
|
||
|
key1 = "value1"
|
||
|
key2 = "value2"
|
||
|
key3 = "value3"
|
||
|
`
|
||
|
type config struct {
|
||
|
Key1 string
|
||
|
Key3 string
|
||
|
}
|
||
|
|
||
|
var conf config
|
||
|
md, err := Decode(blob, &conf)
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
fmt.Printf("Undecoded keys: %q\n", md.Undecoded())
|
||
|
// Output:
|
||
|
// Undecoded keys: ["key2"]
|
||
|
}
|