├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── ast └── ast.go ├── config.go ├── config_test.go ├── decode.go ├── decode_bench_test.go ├── decode_test.go ├── encode.go ├── encode_test.go ├── error.go ├── example_decoder_test.go ├── example_marshaler_rec_test.go ├── example_marshaler_test.go ├── example_text_marshaler_test.go ├── example_util_test.go ├── go.mod ├── go.sum ├── parse.go ├── parse.peg ├── parse.peg.go ├── testdata ├── example.toml ├── marshal-arraytable-empty.toml ├── marshal-funkymapkeys.toml ├── marshal-key-escape.toml ├── marshal-marshaler.toml ├── marshal-marshalerrec.toml ├── marshal-teststruct.toml ├── test.toml ├── unmarshal-array-1.toml ├── unmarshal-array-2.toml ├── unmarshal-array-3.toml ├── unmarshal-array-4.toml ├── unmarshal-array-5.toml ├── unmarshal-array-6.toml ├── unmarshal-array-multitype.toml ├── unmarshal-arraytable-conflict-1.toml ├── unmarshal-arraytable-conflict-2.toml ├── unmarshal-arraytable-conflict-3.toml ├── unmarshal-arraytable-inline.toml ├── unmarshal-arraytable-nested-1.toml ├── unmarshal-arraytable-nested-2.toml ├── unmarshal-arraytable-nested-3.toml ├── unmarshal-arraytable.toml ├── unmarshal-errline-crlf.toml ├── unmarshal-errline-lf.toml ├── unmarshal-interface.toml ├── unmarshal-pointer.toml ├── unmarshal-string-1.toml ├── unmarshal-string-2.toml ├── unmarshal-string-3.toml ├── unmarshal-string-4.toml ├── unmarshal-string-5.toml ├── unmarshal-string-6.toml ├── unmarshal-table-conflict-1.toml ├── unmarshal-table-conflict-2.toml ├── unmarshal-table-conflict-3.toml ├── unmarshal-table-inline-comma-invalid-1.toml ├── unmarshal-table-inline-comma-invalid-2.toml ├── unmarshal-table-newline-req-1.toml ├── unmarshal-table-newline-req-2.toml ├── unmarshal-table-withinline.toml ├── unmarshal-table-withmap.toml ├── unmarshal-textunmarshaler.toml ├── unmarshal-unmarshaler.toml └── unmarshal-unmarshalerrec.toml ├── toml-test-adapter.go ├── toml-test.sh └── util.go /.gitattributes: -------------------------------------------------------------------------------- 1 | /testdata/unmarshal-errline-lf.toml eol=lf 2 | /testdata/unmarshal-errline-crlf.toml eol=crlf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /toml-test 2 | /toml-test-adapter 3 | /peg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.3 5 | - 1.x 6 | 7 | install: 8 | - go get -t -v ./... 9 | 10 | script: 11 | - go test ./... 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Naoya Inada 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TOML parser and encoder library for Golang [![Build Status](https://travis-ci.org/naoina/toml.png?branch=master)](https://travis-ci.org/naoina/toml) 2 | 3 | [TOML](https://github.com/toml-lang/toml) parser and encoder library for [Golang](http://golang.org/). 4 | 5 | This library is compatible with TOML version [v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md). 6 | 7 | ## Installation 8 | 9 | go get -u github.com/naoina/toml 10 | 11 | ## Usage 12 | 13 | The following TOML save as `example.toml`. 14 | 15 | ```toml 16 | # This is a TOML document. Boom. 17 | 18 | title = "TOML Example" 19 | 20 | [owner] 21 | name = "Lance Uppercut" 22 | dob = 1979-05-27T07:32:00-08:00 # First class dates? Why not? 23 | 24 | [database] 25 | server = "192.168.1.1" 26 | ports = [ 8001, 8001, 8002 ] 27 | connection_max = 5000 28 | enabled = true 29 | 30 | [servers] 31 | 32 | # You can indent as you please. Tabs or spaces. TOML don't care. 33 | [servers.alpha] 34 | ip = "10.0.0.1" 35 | dc = "eqdc10" 36 | 37 | [servers.beta] 38 | ip = "10.0.0.2" 39 | dc = "eqdc10" 40 | 41 | [clients] 42 | data = [ ["gamma", "delta"], [1, 2] ] 43 | 44 | # Line breaks are OK when inside arrays 45 | hosts = [ 46 | "alpha", 47 | "omega" 48 | ] 49 | ``` 50 | 51 | Then above TOML will mapping to `tomlConfig` struct using `toml.Unmarshal`. 52 | 53 | ```go 54 | package main 55 | 56 | import ( 57 | "os" 58 | "time" 59 | 60 | "github.com/naoina/toml" 61 | ) 62 | 63 | type tomlConfig struct { 64 | Title string 65 | Owner struct { 66 | Name string 67 | Dob time.Time 68 | } 69 | Database struct { 70 | Server string 71 | Ports []int 72 | ConnectionMax uint 73 | Enabled bool 74 | } 75 | Servers map[string]ServerInfo 76 | Clients struct { 77 | Data [][]interface{} 78 | Hosts []string 79 | } 80 | } 81 | 82 | type ServerInfo struct { 83 | IP net.IP 84 | DC string 85 | } 86 | 87 | func main() { 88 | f, err := os.Open("example.toml") 89 | if err != nil { 90 | panic(err) 91 | } 92 | defer f.Close() 93 | var config tomlConfig 94 | if err := toml.NewDecoder(f).Decode(&config); err != nil { 95 | panic(err) 96 | } 97 | 98 | // then to use the unmarshaled config... 99 | fmt.Println("IP of server 'alpha':", config.Servers["alpha"].IP) 100 | } 101 | ``` 102 | 103 | ## Mappings 104 | 105 | A key and value of TOML will map to the corresponding field. 106 | The fields of struct for mapping must be exported. 107 | 108 | The rules of the mapping of key are following: 109 | 110 | #### Exact matching 111 | 112 | ```toml 113 | timeout_seconds = 256 114 | ``` 115 | 116 | ```go 117 | type Config struct { 118 | Timeout_seconds int 119 | } 120 | ``` 121 | 122 | #### Camelcase matching 123 | 124 | ```toml 125 | server_name = "srv1" 126 | ``` 127 | 128 | ```go 129 | type Config struct { 130 | ServerName string 131 | } 132 | ``` 133 | 134 | #### Uppercase matching 135 | 136 | ```toml 137 | ip = "10.0.0.1" 138 | ``` 139 | 140 | ```go 141 | type Config struct { 142 | IP string 143 | } 144 | ``` 145 | 146 | See the following examples for the value mappings. 147 | 148 | ### String 149 | 150 | ```toml 151 | val = "string" 152 | ``` 153 | 154 | ```go 155 | type Config struct { 156 | Val string 157 | } 158 | ``` 159 | 160 | ### Integer 161 | 162 | ```toml 163 | val = 100 164 | ``` 165 | 166 | ```go 167 | type Config struct { 168 | Val int 169 | } 170 | ``` 171 | 172 | All types that can be used are following: 173 | 174 | * int8 (from `-128` to `127`) 175 | * int16 (from `-32768` to `32767`) 176 | * int32 (from `-2147483648` to `2147483647`) 177 | * int64 (from `-9223372036854775808` to `9223372036854775807`) 178 | * int (same as `int32` on 32bit environment, or `int64` on 64bit environment) 179 | * uint8 (from `0` to `255`) 180 | * uint16 (from `0` to `65535`) 181 | * uint32 (from `0` to `4294967295`) 182 | * uint64 (from `0` to `18446744073709551615`) 183 | * uint (same as `uint32` on 32bit environment, or `uint64` on 64bit environment) 184 | 185 | ### Float 186 | 187 | ```toml 188 | val = 3.1415 189 | ``` 190 | 191 | ```go 192 | type Config struct { 193 | Val float32 194 | } 195 | ``` 196 | 197 | All types that can be used are following: 198 | 199 | * float32 200 | * float64 201 | 202 | ### Boolean 203 | 204 | ```toml 205 | val = true 206 | ``` 207 | 208 | ```go 209 | type Config struct { 210 | Val bool 211 | } 212 | ``` 213 | 214 | ### Datetime 215 | 216 | ```toml 217 | val = 2014-09-28T21:27:39Z 218 | ``` 219 | 220 | ```go 221 | type Config struct { 222 | Val time.Time 223 | } 224 | ``` 225 | 226 | ### Array 227 | 228 | ```toml 229 | val = ["a", "b", "c"] 230 | ``` 231 | 232 | ```go 233 | type Config struct { 234 | Val []string 235 | } 236 | ``` 237 | 238 | Also following examples all can be mapped: 239 | 240 | ```toml 241 | val1 = [1, 2, 3] 242 | val2 = [["a", "b"], ["c", "d"]] 243 | val3 = [[1, 2, 3], ["a", "b", "c"]] 244 | val4 = [[1, 2, 3], [["a", "b"], [true, false]]] 245 | ``` 246 | 247 | ```go 248 | type Config struct { 249 | Val1 []int 250 | Val2 [][]string 251 | Val3 [][]interface{} 252 | Val4 [][]interface{} 253 | } 254 | ``` 255 | 256 | ### Table 257 | 258 | ```toml 259 | [server] 260 | type = "app" 261 | 262 | [server.development] 263 | ip = "10.0.0.1" 264 | 265 | [server.production] 266 | ip = "10.0.0.2" 267 | ``` 268 | 269 | ```go 270 | type Config struct { 271 | Server map[string]Server 272 | } 273 | 274 | type Server struct { 275 | IP string 276 | } 277 | ``` 278 | 279 | You can also use the following struct instead of map of struct. 280 | 281 | ```go 282 | type Config struct { 283 | Server struct { 284 | Development Server 285 | Production Server 286 | } 287 | } 288 | 289 | type Server struct { 290 | IP string 291 | } 292 | ``` 293 | 294 | ### Array of Tables 295 | 296 | ```toml 297 | [[fruit]] 298 | name = "apple" 299 | 300 | [fruit.physical] 301 | color = "red" 302 | shape = "round" 303 | 304 | [[fruit.variety]] 305 | name = "red delicious" 306 | 307 | [[fruit.variety]] 308 | name = "granny smith" 309 | 310 | [[fruit]] 311 | name = "banana" 312 | 313 | [[fruit.variety]] 314 | name = "plantain" 315 | ``` 316 | 317 | ```go 318 | type Config struct { 319 | Fruit []struct { 320 | Name string 321 | Physical struct { 322 | Color string 323 | Shape string 324 | } 325 | Variety []struct { 326 | Name string 327 | } 328 | } 329 | } 330 | ``` 331 | 332 | ### Using the `encoding.TextUnmarshaler` interface 333 | 334 | Package toml supports `encoding.TextUnmarshaler` (and `encoding.TextMarshaler`). You can 335 | use it to apply custom marshaling rules for certain types. The `UnmarshalText` method is 336 | called with the value text found in the TOML input. TOML strings are passed unquoted. 337 | 338 | ```toml 339 | duration = "10s" 340 | ``` 341 | 342 | ```go 343 | import time 344 | 345 | type Duration time.Duration 346 | 347 | // UnmarshalText implements encoding.TextUnmarshaler 348 | func (d *Duration) UnmarshalText(data []byte) error { 349 | duration, err := time.ParseDuration(string(data)) 350 | if err == nil { 351 | *d = Duration(duration) 352 | } 353 | return err 354 | } 355 | 356 | // MarshalText implements encoding.TextMarshaler 357 | func (d Duration) MarshalText() ([]byte, error) { 358 | return []byte(time.Duration(d).String()), nil 359 | } 360 | 361 | type ConfigWithDuration struct { 362 | Duration Duration 363 | } 364 | ``` 365 | ### Using the `toml.UnmarshalerRec` interface 366 | 367 | You can also override marshaling rules specifically for TOML using the `UnmarshalerRec` 368 | and `MarshalerRec` interfaces. These are useful if you want to control how structs or 369 | arrays are handled. You can apply additional validation or set unexported struct fields. 370 | 371 | Note: `encoding.TextUnmarshaler` and `encoding.TextMarshaler` should be preferred for 372 | simple (scalar) values because they're also compatible with other formats like JSON or 373 | YAML. 374 | 375 | [See the UnmarshalerRec example](https://godoc.org/github.com/naoina/toml/#example_UnmarshalerRec). 376 | 377 | ### Using the `toml.Unmarshaler` interface 378 | 379 | If you want to deal with raw TOML syntax, use the `Unmarshaler` and `Marshaler` 380 | interfaces. Their input and output is raw TOML syntax. As such, these interfaces are 381 | useful if you want to handle TOML at the syntax level. 382 | 383 | [See the Unmarshaler example](https://godoc.org/github.com/naoina/toml/#example_Unmarshaler). 384 | 385 | ## API documentation 386 | 387 | See [Godoc](http://godoc.org/github.com/naoina/toml). 388 | 389 | ## License 390 | 391 | MIT 392 | -------------------------------------------------------------------------------- /ast/ast.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type Position struct { 10 | Begin int 11 | End int 12 | } 13 | 14 | type Value interface { 15 | Pos() int 16 | End() int 17 | Source() string 18 | } 19 | 20 | type String struct { 21 | Position Position 22 | Value string 23 | Data []rune 24 | } 25 | 26 | func (s *String) Pos() int { 27 | return s.Position.Begin 28 | } 29 | 30 | func (s *String) End() int { 31 | return s.Position.End 32 | } 33 | 34 | func (s *String) Source() string { 35 | return string(s.Data) 36 | } 37 | 38 | type Integer struct { 39 | Position Position 40 | Value string 41 | Data []rune 42 | } 43 | 44 | func (i *Integer) Pos() int { 45 | return i.Position.Begin 46 | } 47 | 48 | func (i *Integer) End() int { 49 | return i.Position.End 50 | } 51 | 52 | func (i *Integer) Source() string { 53 | return string(i.Data) 54 | } 55 | 56 | func (i *Integer) Int() (int64, error) { 57 | return strconv.ParseInt(i.Value, 0, 64) 58 | } 59 | 60 | // Sign returns -1 when the integer is negative, +1 when it is 61 | // explicitly positive, and 0 otherwise. 62 | func (i *Integer) Sign() int { 63 | if len(i.Value) > 0 { 64 | if i.Value[0] == '-' { 65 | return -1 66 | } else if i.Value[0] == '+' { 67 | return 1 68 | } 69 | } 70 | return 0 71 | } 72 | 73 | type Float struct { 74 | Position Position 75 | Value string 76 | Data []rune 77 | } 78 | 79 | func (f *Float) Pos() int { 80 | return f.Position.Begin 81 | } 82 | 83 | func (f *Float) End() int { 84 | return f.Position.End 85 | } 86 | 87 | func (f *Float) Source() string { 88 | return string(f.Data) 89 | } 90 | 91 | func (f *Float) Float() (float64, error) { 92 | return strconv.ParseFloat(f.Value, 64) 93 | } 94 | 95 | type Boolean struct { 96 | Position Position 97 | Value string 98 | Data []rune 99 | } 100 | 101 | func (b *Boolean) Pos() int { 102 | return b.Position.Begin 103 | } 104 | 105 | func (b *Boolean) End() int { 106 | return b.Position.End 107 | } 108 | 109 | func (b *Boolean) Source() string { 110 | return string(b.Data) 111 | } 112 | 113 | func (b *Boolean) Boolean() (bool, error) { 114 | return strconv.ParseBool(b.Value) 115 | } 116 | 117 | type Datetime struct { 118 | Position Position 119 | Value string 120 | Data []rune 121 | } 122 | 123 | func (d *Datetime) Pos() int { 124 | return d.Position.Begin 125 | } 126 | 127 | func (d *Datetime) End() int { 128 | return d.Position.End 129 | } 130 | 131 | func (d *Datetime) Source() string { 132 | return string(d.Data) 133 | } 134 | 135 | var timeFormats = [...]string{ 136 | "2006-01-02T15:04:05.999999999Z07:00", 137 | "2006-01-02 15:04:05.999999999Z07:00", 138 | "2006-01-02T15:04:05.999999999", 139 | "2006-01-02 15:04:05.999999999", 140 | } 141 | 142 | func (d *Datetime) Time() (time.Time, error) { 143 | switch { 144 | case !strings.Contains(d.Value, ":"): 145 | return time.Parse("2006-01-02", d.Value) 146 | case !strings.Contains(d.Value, "-"): 147 | return time.Parse("15:04:05.999999999", d.Value) 148 | default: 149 | var t time.Time 150 | var err error 151 | for _, format := range timeFormats { 152 | if t, err = time.Parse(format, d.Value); err == nil { 153 | return t, nil 154 | } 155 | } 156 | return t, err 157 | } 158 | } 159 | 160 | type Array struct { 161 | Position Position 162 | Value []Value 163 | Data []rune 164 | } 165 | 166 | func (a *Array) Pos() int { 167 | return a.Position.Begin 168 | } 169 | 170 | func (a *Array) End() int { 171 | return a.Position.End 172 | } 173 | 174 | func (a *Array) Source() string { 175 | return string(a.Data) 176 | } 177 | 178 | type TableType uint8 179 | 180 | const ( 181 | TableTypeNormal TableType = iota 182 | TableTypeArray 183 | TableTypeInline 184 | ) 185 | 186 | var tableTypes = [...]string{ 187 | "normal", 188 | "array", 189 | "inline", 190 | } 191 | 192 | func (t TableType) String() string { 193 | if int(t) < len(tableTypes) { 194 | return tableTypes[t] 195 | } 196 | return "unknown type" 197 | } 198 | 199 | type Table struct { 200 | Position Position 201 | Line int 202 | Name string 203 | Fields map[string]interface{} 204 | Type TableType 205 | Data []rune 206 | } 207 | 208 | func (t *Table) Pos() int { 209 | return t.Position.Begin 210 | } 211 | 212 | func (t *Table) End() int { 213 | return t.Position.End 214 | } 215 | 216 | func (t *Table) Source() string { 217 | return string(t.Data) 218 | } 219 | 220 | type KeyValue struct { 221 | Key string 222 | Value Value 223 | Line int 224 | } 225 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "strings" 7 | 8 | stringutil "github.com/naoina/go-stringutil" 9 | "github.com/naoina/toml/ast" 10 | ) 11 | 12 | // Config contains options for encoding and decoding. 13 | type Config struct { 14 | // NormFieldName is used to match TOML keys to struct fields. The function runs for 15 | // both input keys and struct field names and should return a string that makes the 16 | // two match. You must set this field to use the decoder. 17 | // 18 | // Example: The function in the default config removes _ and lowercases all keys. This 19 | // allows a key called 'api_key' to match the struct field 'APIKey' because both are 20 | // normalized to 'apikey'. 21 | // 22 | // Note that NormFieldName is not used for fields which define a TOML 23 | // key through the struct tag. 24 | NormFieldName func(typ reflect.Type, keyOrField string) string 25 | 26 | // FieldToKey determines the TOML key of a struct field when encoding. 27 | // You must set this field to use the encoder. 28 | // 29 | // Note that FieldToKey is not used for fields which define a TOML 30 | // key through the struct tag. 31 | FieldToKey func(typ reflect.Type, field string) string 32 | 33 | // MissingField, if non-nil, is called when the decoder encounters a key for which no 34 | // matching struct field exists. The default behavior is to return an error. 35 | MissingField func(typ reflect.Type, key string) error 36 | 37 | // WriteEmptyTables instructs the encoder to write all tables, even if they are empty. 38 | // By default, empty tables are not written to the output. Note that empty array 39 | // tables and inline tables are always written. 40 | // 41 | // This setting mostly exists for compatibility with the toml-test tool. 42 | // Don't set this unless you have a good reason for it. 43 | WriteEmptyTables bool 44 | } 45 | 46 | // DefaultConfig contains the default options for encoding and decoding. 47 | // Snake case (i.e. 'foo_bar') is used for key names. 48 | var DefaultConfig = Config{ 49 | NormFieldName: defaultNormFieldName, 50 | FieldToKey: snakeCase, 51 | } 52 | 53 | func defaultNormFieldName(typ reflect.Type, s string) string { 54 | return strings.Replace(strings.ToLower(s), "_", "", -1) 55 | } 56 | 57 | func snakeCase(typ reflect.Type, s string) string { 58 | return stringutil.ToSnakeCase(s) 59 | } 60 | 61 | // NewEncoder returns a new Encoder that writes to w. 62 | // It is shorthand for DefaultConfig.NewEncoder(w). 63 | func NewEncoder(w io.Writer) *Encoder { 64 | return DefaultConfig.NewEncoder(w) 65 | } 66 | 67 | // Marshal returns the TOML encoding of v. 68 | // It is shorthand for DefaultConfig.Marshal(v). 69 | func Marshal(v interface{}) ([]byte, error) { 70 | return DefaultConfig.Marshal(v) 71 | } 72 | 73 | // Unmarshal parses the TOML data and stores the result in the value pointed to by v. 74 | // It is shorthand for DefaultConfig.Unmarshal(data, v). 75 | func Unmarshal(data []byte, v interface{}) error { 76 | return DefaultConfig.Unmarshal(data, v) 77 | } 78 | 79 | // UnmarshalTable applies the contents of an ast.Table to the value pointed at by v. 80 | // It is shorthand for DefaultConfig.UnmarshalTable(t, v). 81 | func UnmarshalTable(t *ast.Table, v interface{}) error { 82 | return DefaultConfig.UnmarshalTable(t, v) 83 | } 84 | 85 | // NewDecoder returns a new Decoder that reads from r. 86 | // It is shorthand for DefaultConfig.NewDecoder(r). 87 | func NewDecoder(r io.Reader) *Decoder { 88 | return DefaultConfig.NewDecoder(r) 89 | } 90 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestConfigNormField(t *testing.T) { 11 | cfg := Config{NormFieldName: func(reflect.Type, string) string { return "a" }} 12 | 13 | var x struct{ B int } 14 | input := []byte(`a = 1`) 15 | if err := cfg.Unmarshal(input, &x); err != nil { 16 | t.Fatal(err) 17 | } 18 | if x.B != 1 { 19 | t.Fatalf("wrong value after Unmarshal: got %d, want %d", x.B, 1) 20 | } 21 | 22 | dec := cfg.NewDecoder(strings.NewReader(`a = 2`)) 23 | if err := dec.Decode(&x); err != nil { 24 | t.Fatal(err) 25 | } 26 | if x.B != 2 { 27 | t.Fatalf("wrong value after Decode: got %d, want %d", x.B, 2) 28 | } 29 | 30 | tbl, _ := Parse([]byte(`a = 3`)) 31 | if err := cfg.UnmarshalTable(tbl, &x); err != nil { 32 | t.Fatal(err) 33 | } 34 | if x.B != 3 { 35 | t.Fatalf("wrong value after UnmarshalTable: got %d, want %d", x.B, 3) 36 | } 37 | } 38 | 39 | func TestConfigFieldToKey(t *testing.T) { 40 | cfg := Config{FieldToKey: func(reflect.Type, string) string { return "A" }} 41 | 42 | x := struct{ B int }{B: 999} 43 | enc, err := cfg.Marshal(&x) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | if !bytes.Equal(enc, []byte("A = 999\n")) { 48 | t.Fatalf(`got %q, want "A = 999"`, enc) 49 | } 50 | } 51 | 52 | func TestConfigMissingField(t *testing.T) { 53 | calls := make(map[string]bool) 54 | cfg := Config{ 55 | NormFieldName: func(rt reflect.Type, field string) string { 56 | return field 57 | }, 58 | MissingField: func(rt reflect.Type, field string) error { 59 | calls[field] = true 60 | return nil 61 | }, 62 | } 63 | 64 | var x struct{ B int } 65 | input := []byte(` 66 | A = 1 67 | B = 2 68 | `) 69 | if err := cfg.Unmarshal(input, &x); err != nil { 70 | t.Fatal(err) 71 | } 72 | if x.B != 2 { 73 | t.Errorf("wrong value after Unmarshal: got %d, want %d", x.B, 1) 74 | } 75 | if !calls["A"] { 76 | t.Error("MissingField not called for 'A'") 77 | } 78 | if calls["B"] { 79 | t.Error("MissingField called for 'B'") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | // Package toml encodes and decodes the TOML configuration format using reflection. 2 | // 3 | // This library is compatible with TOML version v0.4.0. 4 | package toml 5 | 6 | import ( 7 | "encoding" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "reflect" 12 | "strconv" 13 | "time" 14 | 15 | "github.com/naoina/toml/ast" 16 | ) 17 | 18 | var timeType = reflect.TypeOf(time.Time{}) 19 | 20 | // Unmarshal parses the TOML data and stores the result in the value pointed to by v. 21 | // 22 | // Unmarshal will mapped to v that according to following rules: 23 | // 24 | // TOML strings to string 25 | // TOML integers to any int type 26 | // TOML floats to float32 or float64 27 | // TOML booleans to bool 28 | // TOML datetimes to time.Time 29 | // TOML arrays to any type of slice 30 | // TOML tables to struct or map 31 | // TOML array tables to slice of struct or map 32 | func (cfg *Config) Unmarshal(data []byte, v interface{}) error { 33 | table, err := Parse(data) 34 | if err != nil { 35 | return err 36 | } 37 | if err := cfg.UnmarshalTable(table, v); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | // A Decoder reads and decodes TOML from an input stream. 44 | type Decoder struct { 45 | r io.Reader 46 | cfg *Config 47 | } 48 | 49 | // NewDecoder returns a new Decoder that reads from r. 50 | // Note that it reads all from r before parsing it. 51 | func (cfg *Config) NewDecoder(r io.Reader) *Decoder { 52 | return &Decoder{r, cfg} 53 | } 54 | 55 | // Decode parses the TOML data from its input and stores it in the value pointed to by v. 56 | // See the documentation for Unmarshal for details about the conversion of TOML into a Go value. 57 | func (d *Decoder) Decode(v interface{}) error { 58 | b, err := ioutil.ReadAll(d.r) 59 | if err != nil { 60 | return err 61 | } 62 | return d.cfg.Unmarshal(b, v) 63 | } 64 | 65 | // UnmarshalerRec may be implemented by types to customize their behavior when being 66 | // unmarshaled from TOML. You can use it to implement custom validation or to set 67 | // unexported fields. 68 | // 69 | // UnmarshalTOML receives a function that can be called to unmarshal the original TOML 70 | // value into a field or variable. It is safe to call the function more than once if 71 | // necessary. 72 | type UnmarshalerRec interface { 73 | UnmarshalTOML(fn func(interface{}) error) error 74 | } 75 | 76 | // Unmarshaler can be used to capture and process raw TOML source of a table or value. 77 | // UnmarshalTOML must copy the input if it wishes to retain it after returning. 78 | // 79 | // Note: this interface is retained for backwards compatibility. You probably want 80 | // to implement encoding.TextUnmarshaler or UnmarshalerRec instead. 81 | type Unmarshaler interface { 82 | UnmarshalTOML(input []byte) error 83 | } 84 | 85 | // UnmarshalTable applies the contents of an ast.Table to the value pointed at by v. 86 | // 87 | // UnmarshalTable will mapped to v that according to following rules: 88 | // 89 | // TOML strings to string 90 | // TOML integers to any int type 91 | // TOML floats to float32 or float64 92 | // TOML booleans to bool 93 | // TOML datetimes to time.Time 94 | // TOML arrays to any type of slice 95 | // TOML tables to struct or map 96 | // TOML array tables to slice of struct or map 97 | func (cfg *Config) UnmarshalTable(t *ast.Table, v interface{}) error { 98 | rv := reflect.ValueOf(v) 99 | toplevelMap := rv.Kind() == reflect.Map 100 | if (!toplevelMap && rv.Kind() != reflect.Ptr) || rv.IsNil() { 101 | return &invalidUnmarshalError{reflect.TypeOf(v)} 102 | } 103 | return unmarshalTable(cfg, rv, t, toplevelMap) 104 | } 105 | 106 | // used for UnmarshalerRec. 107 | func unmarshalTableOrValue(cfg *Config, rv reflect.Value, av interface{}) error { 108 | if (rv.Kind() != reflect.Ptr && rv.Kind() != reflect.Map) || rv.IsNil() { 109 | return &invalidUnmarshalError{rv.Type()} 110 | } 111 | rv = indirect(rv) 112 | 113 | switch av := av.(type) { 114 | case *ast.KeyValue, *ast.Table, []*ast.Table: 115 | if err := unmarshalField(cfg, rv, av); err != nil { 116 | return lineError(fieldLineNumber(av), err) 117 | } 118 | return nil 119 | case ast.Value: 120 | return setValue(cfg, rv, av) 121 | default: 122 | panic(fmt.Sprintf("BUG: unhandled AST node type %T", av)) 123 | } 124 | } 125 | 126 | // unmarshalTable unmarshals the fields of a table into a struct or map. 127 | // 128 | // toplevelMap is true when rv is an (unadressable) map given to UnmarshalTable. In this 129 | // (special) case, the map is used as-is instead of creating a new map. 130 | func unmarshalTable(cfg *Config, rv reflect.Value, t *ast.Table, toplevelMap bool) error { 131 | rv = indirect(rv) 132 | if handled, err := setUnmarshaler(cfg, rv, t); handled { 133 | return lineError(t.Line, err) 134 | } 135 | 136 | switch { 137 | case rv.Kind() == reflect.Struct: 138 | fc := makeFieldCache(cfg, rv.Type()) 139 | for key, fieldAst := range t.Fields { 140 | fv, fieldName, err := fc.findField(cfg, rv, key) 141 | if err != nil { 142 | return lineError(fieldLineNumber(fieldAst), err) 143 | } 144 | if fv.IsValid() { 145 | if err := unmarshalField(cfg, fv, fieldAst); err != nil { 146 | return lineErrorField(fieldLineNumber(fieldAst), rv.Type().String()+"."+fieldName, err) 147 | } 148 | } 149 | } 150 | case rv.Kind() == reflect.Map || isEface(rv): 151 | m := rv 152 | if !toplevelMap { 153 | if rv.Kind() == reflect.Interface { 154 | m = reflect.ValueOf(make(map[string]interface{})) 155 | } else { 156 | m = reflect.MakeMap(rv.Type()) 157 | } 158 | } 159 | elemtyp := m.Type().Elem() 160 | for key, fieldAst := range t.Fields { 161 | kv, err := unmarshalMapKey(m.Type().Key(), key) 162 | if err != nil { 163 | return lineError(fieldLineNumber(fieldAst), err) 164 | } 165 | fv := reflect.New(elemtyp).Elem() 166 | if err := unmarshalField(cfg, fv, fieldAst); err != nil { 167 | return lineError(fieldLineNumber(fieldAst), err) 168 | } 169 | m.SetMapIndex(kv, fv) 170 | } 171 | if !toplevelMap { 172 | rv.Set(m) 173 | } 174 | default: 175 | return lineError(t.Line, &unmarshalTypeError{"table", "struct or map", rv.Type()}) 176 | } 177 | return nil 178 | } 179 | 180 | func fieldLineNumber(fieldAst interface{}) int { 181 | switch av := fieldAst.(type) { 182 | case *ast.KeyValue: 183 | return av.Line 184 | case *ast.Table: 185 | return av.Line 186 | case []*ast.Table: 187 | return av[0].Line 188 | default: 189 | panic(fmt.Sprintf("BUG: unhandled node type %T", fieldAst)) 190 | } 191 | } 192 | 193 | // unmarshalField is called for struct fields and map entries. 194 | // rv is the value that should be set. 195 | func unmarshalField(cfg *Config, rv reflect.Value, fieldAst interface{}) error { 196 | switch av := fieldAst.(type) { 197 | case *ast.KeyValue: 198 | return setValue(cfg, rv, av.Value) 199 | case *ast.Table: 200 | return unmarshalTable(cfg, rv, av, false) 201 | case []*ast.Table: 202 | rv = indirect(rv) 203 | if handled, err := setUnmarshaler(cfg, rv, fieldAst); handled { 204 | return err 205 | } 206 | var slice reflect.Value 207 | switch { 208 | case rv.Kind() == reflect.Slice: 209 | slice = reflect.MakeSlice(rv.Type(), len(av), len(av)) 210 | case isEface(rv): 211 | slice = reflect.ValueOf(make([]interface{}, len(av))) 212 | default: 213 | return &unmarshalTypeError{"array table", "slice", rv.Type()} 214 | } 215 | for i, tbl := range av { 216 | vv := reflect.New(slice.Type().Elem()).Elem() 217 | if err := unmarshalTable(cfg, vv, tbl, false); err != nil { 218 | return err 219 | } 220 | slice.Index(i).Set(vv) 221 | } 222 | rv.Set(slice) 223 | default: 224 | panic(fmt.Sprintf("BUG: unhandled AST node type %T", av)) 225 | } 226 | return nil 227 | } 228 | 229 | func unmarshalMapKey(typ reflect.Type, key string) (reflect.Value, error) { 230 | rv := reflect.New(typ).Elem() 231 | if u, ok := rv.Addr().Interface().(encoding.TextUnmarshaler); ok { 232 | return rv, u.UnmarshalText([]byte(key)) 233 | } 234 | switch typ.Kind() { 235 | case reflect.String: 236 | rv.SetString(key) 237 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 238 | i, err := strconv.ParseInt(key, 10, int(typ.Size()*8)) 239 | if err != nil { 240 | return rv, convertNumError(typ.Kind(), err) 241 | } 242 | rv.SetInt(i) 243 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 244 | i, err := strconv.ParseUint(key, 10, int(typ.Size()*8)) 245 | if err != nil { 246 | return rv, convertNumError(typ.Kind(), err) 247 | } 248 | rv.SetUint(i) 249 | default: 250 | return rv, fmt.Errorf("invalid map key type %s", typ) 251 | } 252 | return rv, nil 253 | } 254 | 255 | func setValue(cfg *Config, lhs reflect.Value, val ast.Value) error { 256 | lhs = indirect(lhs) 257 | if handled, err := setUnmarshaler(cfg, lhs, val); handled { 258 | return err 259 | } 260 | if handled, err := setTextUnmarshaler(lhs, val); handled { 261 | return err 262 | } 263 | switch v := val.(type) { 264 | case *ast.Integer: 265 | return setInt(lhs, v) 266 | case *ast.Float: 267 | return setFloat(lhs, v) 268 | case *ast.String: 269 | return setString(lhs, v) 270 | case *ast.Boolean: 271 | return setBoolean(lhs, v) 272 | case *ast.Datetime: 273 | return setDatetime(lhs, v) 274 | case *ast.Array: 275 | return setArray(cfg, lhs, v) 276 | case *ast.Table: 277 | return unmarshalTable(cfg, lhs, v, false) 278 | default: 279 | panic(fmt.Sprintf("BUG: unhandled node type %T", v)) 280 | } 281 | } 282 | 283 | func indirect(rv reflect.Value) reflect.Value { 284 | for rv.Kind() == reflect.Ptr { 285 | if rv.IsNil() { 286 | rv.Set(reflect.New(rv.Type().Elem())) 287 | } 288 | rv = rv.Elem() 289 | } 290 | return rv 291 | } 292 | 293 | func setUnmarshaler(cfg *Config, lhs reflect.Value, av interface{}) (bool, error) { 294 | if lhs.CanAddr() { 295 | if u, ok := lhs.Addr().Interface().(UnmarshalerRec); ok { 296 | err := u.UnmarshalTOML(func(v interface{}) error { 297 | return unmarshalTableOrValue(cfg, reflect.ValueOf(v), av) 298 | }) 299 | return true, err 300 | } 301 | if u, ok := lhs.Addr().Interface().(Unmarshaler); ok { 302 | return true, u.UnmarshalTOML(unmarshalerSource(av)) 303 | } 304 | } 305 | return false, nil 306 | } 307 | 308 | func unmarshalerSource(av interface{}) []byte { 309 | var source []byte 310 | switch av := av.(type) { 311 | case []*ast.Table: 312 | for i, tab := range av { 313 | source = append(source, tab.Source()...) 314 | if i != len(av)-1 { 315 | source = append(source, '\n') 316 | } 317 | } 318 | case ast.Value: 319 | source = []byte(av.Source()) 320 | default: 321 | panic(fmt.Sprintf("BUG: unhandled node type %T", av)) 322 | } 323 | return source 324 | } 325 | 326 | func setTextUnmarshaler(lhs reflect.Value, val ast.Value) (bool, error) { 327 | if !lhs.CanAddr() { 328 | return false, nil 329 | } 330 | u, ok := lhs.Addr().Interface().(encoding.TextUnmarshaler) 331 | if !ok || lhs.Type() == timeType { 332 | return false, nil 333 | } 334 | var data string 335 | switch val := val.(type) { 336 | case *ast.Array: 337 | return true, &unmarshalTypeError{"array", "", lhs.Type()} 338 | case *ast.Table: 339 | return true, &unmarshalTypeError{"table", "", lhs.Type()} 340 | case *ast.String: 341 | data = val.Value 342 | case *ast.Integer: 343 | data = val.Value 344 | case *ast.Float: 345 | data = val.Value 346 | default: 347 | data = val.Source() 348 | } 349 | return true, u.UnmarshalText([]byte(data)) 350 | } 351 | 352 | func setInt(fv reflect.Value, v *ast.Integer) error { 353 | k := fv.Kind() 354 | switch { 355 | case k >= reflect.Int && k <= reflect.Int64: 356 | i, err := strconv.ParseInt(v.Value, 0, int(fv.Type().Size()*8)) 357 | if err != nil { 358 | return convertNumError(fv.Kind(), err) 359 | } 360 | fv.SetInt(i) 361 | case k >= reflect.Uint && k <= reflect.Uintptr: 362 | if v.Sign() < 0 { 363 | return &unmarshalTypeError{"integer < 0", "", fv.Type()} 364 | } 365 | i, err := strconv.ParseUint(v.Value, 0, int(fv.Type().Size()*8)) 366 | if err != nil { 367 | return convertNumError(fv.Kind(), err) 368 | } 369 | fv.SetUint(i) 370 | case isEface(fv): 371 | i, err := strconv.ParseInt(v.Value, 0, 64) 372 | if err != nil { 373 | return convertNumError(reflect.Int64, err) 374 | } 375 | fv.Set(reflect.ValueOf(i)) 376 | default: 377 | return &unmarshalTypeError{"integer", "", fv.Type()} 378 | } 379 | return nil 380 | } 381 | 382 | func setFloat(fv reflect.Value, v *ast.Float) error { 383 | f, err := v.Float() 384 | if err != nil { 385 | return err 386 | } 387 | switch { 388 | case fv.Kind() == reflect.Float32 || fv.Kind() == reflect.Float64: 389 | if fv.OverflowFloat(f) { 390 | return &overflowError{fv.Kind(), v.Value} 391 | } 392 | fv.SetFloat(f) 393 | case isEface(fv): 394 | fv.Set(reflect.ValueOf(f)) 395 | default: 396 | return &unmarshalTypeError{"float", "", fv.Type()} 397 | } 398 | return nil 399 | } 400 | 401 | func setString(fv reflect.Value, v *ast.String) error { 402 | switch { 403 | case fv.Kind() == reflect.String: 404 | fv.SetString(v.Value) 405 | case isEface(fv): 406 | fv.Set(reflect.ValueOf(v.Value)) 407 | default: 408 | return &unmarshalTypeError{"string", "", fv.Type()} 409 | } 410 | return nil 411 | } 412 | 413 | func setBoolean(fv reflect.Value, v *ast.Boolean) error { 414 | b, _ := v.Boolean() 415 | switch { 416 | case fv.Kind() == reflect.Bool: 417 | fv.SetBool(b) 418 | case isEface(fv): 419 | fv.Set(reflect.ValueOf(b)) 420 | default: 421 | return &unmarshalTypeError{"boolean", "", fv.Type()} 422 | } 423 | return nil 424 | } 425 | 426 | func setDatetime(rv reflect.Value, v *ast.Datetime) error { 427 | t, err := v.Time() 428 | if err != nil { 429 | return err 430 | } 431 | if !timeType.AssignableTo(rv.Type()) { 432 | return &unmarshalTypeError{"datetime", "", rv.Type()} 433 | } 434 | rv.Set(reflect.ValueOf(t)) 435 | return nil 436 | } 437 | 438 | func setArray(cfg *Config, rv reflect.Value, v *ast.Array) error { 439 | var slicetyp reflect.Type 440 | switch { 441 | case rv.Kind() == reflect.Slice: 442 | slicetyp = rv.Type() 443 | case isEface(rv): 444 | slicetyp = reflect.SliceOf(rv.Type()) 445 | default: 446 | return &unmarshalTypeError{"array", "slice", rv.Type()} 447 | } 448 | 449 | if len(v.Value) == 0 { 450 | // Ensure defined slices are always set to a non-nil value. 451 | rv.Set(reflect.MakeSlice(slicetyp, 0, 0)) 452 | return nil 453 | } 454 | 455 | slice := reflect.MakeSlice(slicetyp, len(v.Value), len(v.Value)) 456 | typ := slicetyp.Elem() 457 | for i, vv := range v.Value { 458 | tmp := reflect.New(typ).Elem() 459 | if err := setValue(cfg, tmp, vv); err != nil { 460 | return err 461 | } 462 | slice.Index(i).Set(tmp) 463 | } 464 | rv.Set(slice) 465 | return nil 466 | } 467 | 468 | func isEface(rv reflect.Value) bool { 469 | return rv.Kind() == reflect.Interface && rv.Type().NumMethod() == 0 470 | } 471 | -------------------------------------------------------------------------------- /decode_bench_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkUnmarshal(b *testing.B) { 8 | var v testStruct 9 | data := loadTestData("test.toml") 10 | b.ResetTimer() 11 | b.SetBytes(int64(len(data))) 12 | 13 | for i := 0; i < b.N; i++ { 14 | if err := Unmarshal(data, &v); err != nil { 15 | b.Fatal(err) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /decode_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "math" 8 | "math/big" 9 | "path/filepath" 10 | "reflect" 11 | "testing" 12 | "time" 13 | 14 | "github.com/kylelemons/godebug/pretty" 15 | ) 16 | 17 | func loadTestData(file string) []byte { 18 | f := filepath.Join("testdata", file) 19 | data, err := ioutil.ReadFile(f) 20 | if err != nil { 21 | panic(err) 22 | } 23 | return data 24 | } 25 | 26 | func mustTime(tm time.Time, err error) time.Time { 27 | if err != nil { 28 | panic(err) 29 | } 30 | return tm 31 | } 32 | 33 | type Name struct { 34 | First string 35 | Last string 36 | } 37 | type Inline struct { 38 | Name Name 39 | Point map[string]int 40 | } 41 | type Subtable struct { 42 | Key string 43 | } 44 | type Table struct { 45 | Key string 46 | Subtable Subtable 47 | Inline Inline 48 | } 49 | type W struct { 50 | } 51 | type Z struct { 52 | W W 53 | } 54 | type Y struct { 55 | Z Z 56 | } 57 | type X struct { 58 | Y Y 59 | } 60 | type Basic struct { 61 | Basic string 62 | } 63 | type Continued struct { 64 | Key1 string 65 | Key2 string 66 | Key3 string 67 | } 68 | type Multiline struct { 69 | Key1 string 70 | Key2 string 71 | Key3 string 72 | Continued Continued 73 | } 74 | type LiteralMultiline struct { 75 | Regex2 string 76 | Lines string 77 | } 78 | type Literal struct { 79 | Winpath string 80 | Winpath2 string 81 | Quoted string 82 | Regex string 83 | Multiline LiteralMultiline 84 | } 85 | type String struct { 86 | Basic Basic 87 | Multiline Multiline 88 | Literal Literal 89 | } 90 | type IntegerUnderscores struct { 91 | Key1 int 92 | Key2 int 93 | Key3 int 94 | } 95 | type Integer struct { 96 | Key1 int 97 | Key2 int 98 | Key3 int 99 | Key4 int 100 | Underscores IntegerUnderscores 101 | } 102 | type Fractional struct { 103 | Key1 float64 104 | Key2 float64 105 | Key3 float64 106 | } 107 | type Exponent struct { 108 | Key1 float64 109 | Key2 float64 110 | Key3 float64 111 | } 112 | type Both struct { 113 | Key float64 114 | } 115 | type FloatUnderscores struct { 116 | Key1 float64 117 | Key2 float64 118 | } 119 | type Float struct { 120 | Fractional Fractional 121 | Exponent Exponent 122 | Both Both 123 | Underscores FloatUnderscores 124 | } 125 | type Boolean struct { 126 | True bool 127 | False bool 128 | } 129 | type Datetime struct { 130 | Key1 time.Time 131 | Key2 time.Time 132 | Key3 time.Time 133 | } 134 | type Array struct { 135 | Key1 []int 136 | Key2 []string 137 | Key3 [][]int 138 | Key4 [][]interface{} 139 | Key5 []int 140 | Key6 []int 141 | } 142 | type Product struct { 143 | Name string `toml:",omitempty"` 144 | Sku int64 `toml:",omitempty"` 145 | Color string `toml:",omitempty"` 146 | } 147 | type Physical struct { 148 | Color string 149 | Shape string 150 | } 151 | type Variety struct { 152 | Name string 153 | } 154 | type Fruit struct { 155 | Name string 156 | Physical Physical 157 | Variety []Variety 158 | } 159 | type testStruct struct { 160 | Table Table 161 | X X 162 | String String 163 | Integer Integer 164 | Float Float 165 | Boolean Boolean 166 | Datetime Datetime 167 | Array Array 168 | Products []Product 169 | Fruit []Fruit 170 | } 171 | 172 | func theTestStruct() *testStruct { 173 | return &testStruct{ 174 | Table: Table{ 175 | Key: "value", 176 | Subtable: Subtable{ 177 | Key: "another value", 178 | }, 179 | Inline: Inline{ 180 | Name: Name{ 181 | First: "Tom", 182 | Last: "Preston-Werner", 183 | }, 184 | Point: map[string]int{ 185 | "x": 1, 186 | "y": 2, 187 | }, 188 | }, 189 | }, 190 | X: X{}, 191 | String: String{ 192 | Basic: Basic{ 193 | Basic: "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF.", 194 | }, 195 | Multiline: Multiline{ 196 | Key1: "One\nTwo", 197 | Key2: "One\nTwo", 198 | Key3: "One\nTwo", 199 | Continued: Continued{ 200 | Key1: "The quick brown fox jumps over the lazy dog.", 201 | Key2: "The quick brown fox jumps over the lazy dog.", 202 | Key3: "The quick brown fox jumps over the lazy dog.", 203 | }, 204 | }, 205 | Literal: Literal{ 206 | Winpath: `C:\Users\nodejs\templates`, 207 | Winpath2: `\\ServerX\admin$\system32\`, 208 | Quoted: `Tom "Dubs" Preston-Werner`, 209 | Regex: `<\i\c*\s*>`, 210 | Multiline: LiteralMultiline{ 211 | Regex2: `I [dw]on't need \d{2} apples`, 212 | Lines: "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n", 213 | }, 214 | }, 215 | }, 216 | Integer: Integer{ 217 | Key1: 99, 218 | Key2: 42, 219 | Key3: 0, 220 | Key4: -17, 221 | Underscores: IntegerUnderscores{ 222 | Key1: 1000, 223 | Key2: 5349221, 224 | Key3: 12345, 225 | }, 226 | }, 227 | Float: Float{ 228 | Fractional: Fractional{ 229 | Key1: 1.0, 230 | Key2: 3.1415, 231 | Key3: -0.01, 232 | }, 233 | Exponent: Exponent{ 234 | Key1: 5e22, 235 | Key2: 1e6, 236 | Key3: -2e-2, 237 | }, 238 | Both: Both{ 239 | Key: 6.626e-34, 240 | }, 241 | Underscores: FloatUnderscores{ 242 | Key1: 9224617.445991228313, 243 | Key2: 1e100, 244 | }, 245 | }, 246 | Boolean: Boolean{ 247 | True: true, 248 | False: false, 249 | }, 250 | Datetime: Datetime{ 251 | Key1: mustTime(time.Parse(time.RFC3339Nano, "1979-05-27T07:32:00Z")), 252 | Key2: mustTime(time.Parse(time.RFC3339Nano, "1979-05-27T00:32:00-07:00")), 253 | Key3: mustTime(time.Parse(time.RFC3339Nano, "1979-05-27T00:32:00.999999-07:00")), 254 | }, 255 | Array: Array{ 256 | Key1: []int{1, 2, 3}, 257 | Key2: []string{"red", "yellow", "green"}, 258 | Key3: [][]int{{1, 2}, {3, 4, 5}}, 259 | Key4: [][]interface{}{{int64(1), int64(2)}, {"a", "b", "c"}}, 260 | Key5: []int{1, 2, 3}, 261 | Key6: []int{1, 2}, 262 | }, 263 | Products: []Product{ 264 | {Name: "Hammer", Sku: 738594937}, 265 | {}, 266 | {Name: "Nail", Sku: 284758393, Color: "gray"}, 267 | }, 268 | Fruit: []Fruit{ 269 | { 270 | Name: "apple", 271 | Physical: Physical{ 272 | Color: "red", 273 | Shape: "round", 274 | }, 275 | Variety: []Variety{ 276 | {Name: "red delicious"}, 277 | {Name: "granny smith"}, 278 | }, 279 | }, 280 | { 281 | Name: "banana", 282 | Variety: []Variety{ 283 | {Name: "plantain"}, 284 | }, 285 | }, 286 | }, 287 | } 288 | } 289 | 290 | func TestUnmarshal(t *testing.T) { 291 | testUnmarshal(t, []testcase{ 292 | { 293 | data: string(loadTestData("test.toml")), 294 | expect: theTestStruct(), 295 | }, 296 | }) 297 | } 298 | 299 | type testcase struct { 300 | data string 301 | err error 302 | expect interface{} 303 | } 304 | 305 | func testUnmarshal(t *testing.T, testcases []testcase) { 306 | for _, test := range testcases { 307 | // Create a test value of the same type as expect. 308 | typ := reflect.TypeOf(test.expect) 309 | var val interface{} 310 | if typ.Kind() == reflect.Map { 311 | val = reflect.MakeMap(typ).Interface() 312 | } else if typ.Kind() == reflect.Ptr { 313 | val = reflect.New(typ.Elem()).Interface() 314 | } else { 315 | panic("invalid 'expect' type " + typ.String()) 316 | } 317 | 318 | err := Unmarshal([]byte(test.data), val) 319 | if !reflect.DeepEqual(err, test.err) { 320 | t.Errorf("Error mismatch for input:\n%s\ngot: %+v\nwant: %+v", test.data, err, test.err) 321 | } 322 | if err == nil && !reflect.DeepEqual(val, test.expect) { 323 | t.Errorf("Unmarshal value mismatch for input:\n%s\ndiff:\n%s", test.data, pretty.Compare(val, test.expect)) 324 | } 325 | } 326 | } 327 | 328 | func TestUnmarshal_WithString(t *testing.T) { 329 | type testStruct struct { 330 | Str string 331 | Key1 string 332 | Key2 string 333 | Key3 string 334 | Winpath string 335 | Winpath2 string 336 | Quoted string 337 | Regex string 338 | Regex2 string 339 | Lines string 340 | } 341 | testUnmarshal(t, []testcase{ 342 | { 343 | data: `str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."`, 344 | expect: &testStruct{ 345 | Str: "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF.", 346 | }, 347 | }, 348 | { 349 | data: string(loadTestData("unmarshal-string-1.toml")), 350 | expect: &testStruct{Key1: "One\nTwo", Key2: "One\nTwo", Key3: "One\nTwo"}, 351 | }, 352 | { 353 | data: string(loadTestData("unmarshal-string-2.toml")), 354 | expect: &testStruct{ 355 | Key1: "The quick brown fox jumps over the lazy dog.", 356 | Key2: "The quick brown fox jumps over the lazy dog.", 357 | Key3: "The quick brown fox jumps over the lazy dog.", 358 | }, 359 | }, 360 | { 361 | data: string(loadTestData("unmarshal-string-3.toml")), 362 | expect: &testStruct{ 363 | Winpath: `C:\Users\nodejs\templates`, 364 | Winpath2: `\\ServerX\admin$\system32\`, 365 | Quoted: `Tom "Dubs" Preston-Werner`, 366 | Regex: `<\i\c*\s*>`, 367 | }, 368 | }, 369 | { 370 | data: string(loadTestData("unmarshal-string-4.toml")), 371 | expect: &testStruct{ 372 | Regex2: `I [dw]on't need \d{2} apples`, 373 | Lines: "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n", 374 | }, 375 | }, 376 | { 377 | data: string(loadTestData("unmarshal-string-5.toml")), 378 | expect: &testStruct{ 379 | Key1: `I dare to say "this is valid TOML", and I'm not joking`, 380 | Key2: `I dare to say 'this is valid TOML', and I'm not joking`, 381 | Key3: `I dare to say 'this is valid TOML', and I'm not joking`, 382 | }, 383 | }, 384 | { 385 | data: string(loadTestData("unmarshal-string-6.toml")), 386 | expect: &testStruct{ 387 | Key1: "\t", 678 | map[string]interface{}{ 679 | "name": "Baz Qux", 680 | "email": "bazqux@example.com", 681 | "url": "https://example.com/bazqux", 682 | }, 683 | }, 684 | }, 685 | }, 686 | // // whitespace + comments 687 | {string(loadTestData("unmarshal-array-1.toml")), nil, &arrays{Ints: []int{1, 2, 3}}}, 688 | {string(loadTestData("unmarshal-array-2.toml")), nil, &arrays{Ints: []int{1, 2, 3}}}, 689 | {string(loadTestData("unmarshal-array-3.toml")), nil, &arrays{Ints: []int{1, 2, 3}}}, 690 | {string(loadTestData("unmarshal-array-4.toml")), nil, &arrays{Ints: []int{1, 2, 3}}}, 691 | {string(loadTestData("unmarshal-array-5.toml")), nil, &arrays{Ints: []int{1, 2, 3}}}, 692 | {string(loadTestData("unmarshal-array-6.toml")), nil, &arrays{Ints: []int{1, 2, 3}}}, 693 | // parse errors 694 | {`ints = [ , ]`, lineError(1, errParse), &arrays{}}, 695 | {`ints = [ , 1 ]`, lineError(1, errParse), &arrays{}}, 696 | {`ints = [ 1 2 ]`, lineError(1, errParse), &arrays{}}, 697 | {`ints = [ 1 , , 2 ]`, lineError(1, errParse), &arrays{}}, 698 | }) 699 | } 700 | 701 | func TestUnmarshal_WithTable(t *testing.T) { 702 | type W struct{} 703 | type Z struct { 704 | W W 705 | } 706 | type Y struct { 707 | Z Z 708 | } 709 | type X struct { 710 | Y Y 711 | } 712 | type A struct { 713 | D int 714 | B struct { 715 | C int 716 | } 717 | } 718 | type testStruct struct { 719 | Table struct { 720 | Key string 721 | } 722 | Dog struct { 723 | Tater struct{} 724 | } 725 | X X 726 | A A 727 | } 728 | type testIgnoredFieldStruct struct { 729 | Ignored string `toml:"-"` 730 | } 731 | type testNamedFieldStruct struct { 732 | Named string `toml:"Named_Field"` 733 | } 734 | type testQuotedKeyStruct struct { 735 | Dog struct { 736 | TaterMan struct { 737 | Type string 738 | } `toml:"tater.man"` 739 | } 740 | } 741 | type testQuotedKeyWithWhitespaceStruct struct { 742 | Dog struct { 743 | TaterMan struct { 744 | Type string 745 | } `toml:"tater . man"` 746 | } 747 | } 748 | type testStructWithMap struct { 749 | Servers map[string]struct { 750 | IP string 751 | DC string 752 | } 753 | } 754 | type withTableArray struct { 755 | Tabarray []map[string]string 756 | } 757 | 758 | testUnmarshal(t, []testcase{ 759 | {`[table]`, nil, &testStruct{}}, 760 | {`[table] 761 | key = "value"`, nil, 762 | &testStruct{ 763 | Table: struct { 764 | Key string 765 | }{ 766 | Key: "value", 767 | }, 768 | }}, 769 | {`[dog.tater]`, nil, 770 | &testStruct{ 771 | Dog: struct { 772 | Tater struct{} 773 | }{ 774 | Tater: struct{}{}, 775 | }, 776 | }}, 777 | {`[dog."tater.man"] 778 | type = "pug"`, nil, 779 | &testQuotedKeyStruct{ 780 | Dog: struct { 781 | TaterMan struct { 782 | Type string 783 | } `toml:"tater.man"` 784 | }{ 785 | TaterMan: struct { 786 | Type string 787 | }{ 788 | Type: "pug", 789 | }, 790 | }, 791 | }}, 792 | {`[dog."tater . man"] 793 | type = "pug"`, nil, 794 | &testQuotedKeyWithWhitespaceStruct{ 795 | Dog: struct { 796 | TaterMan struct { 797 | Type string 798 | } `toml:"tater . man"` 799 | }{ 800 | TaterMan: struct { 801 | Type string 802 | }{ 803 | Type: "pug", 804 | }, 805 | }, 806 | }}, 807 | {`[x.y.z.w] # for this to work`, nil, 808 | &testStruct{ 809 | X: X{}, 810 | }}, 811 | {`[ x . y . z . w ]`, nil, 812 | &testStruct{ 813 | X: X{}, 814 | }}, 815 | {`[ x . "y" . z . "w" ]`, nil, 816 | &testStruct{ 817 | X: X{}, 818 | }}, 819 | {`table = {}`, nil, &testStruct{}}, 820 | {`table = { key = "value" }`, nil, &testStruct{ 821 | Table: struct { 822 | Key string 823 | }{ 824 | Key: "value", 825 | }, 826 | }}, 827 | {`x = { y = { "z" = { w = {} } } }`, nil, &testStruct{X: X{}}}, 828 | {`[a.b] 829 | c = 1 830 | 831 | [a] 832 | d = 2`, nil, 833 | &testStruct{ 834 | A: struct { 835 | D int 836 | B struct { 837 | C int 838 | } 839 | }{ 840 | D: 2, 841 | B: struct { 842 | C int 843 | }{ 844 | C: 1, 845 | }, 846 | }, 847 | }}, 848 | { 849 | data: `Named_Field = "value"`, 850 | expect: &testNamedFieldStruct{Named: "value"}, 851 | }, 852 | { 853 | data: string(loadTestData("unmarshal-table-withmap.toml")), 854 | expect: &testStructWithMap{ 855 | Servers: map[string]struct { 856 | IP string 857 | DC string 858 | }{ 859 | "alpha": {IP: "10.0.0.1", DC: "eqdc10"}, 860 | "beta": {IP: "10.0.0.2", DC: "eqdc10"}, 861 | }, 862 | }, 863 | }, 864 | { 865 | data: string(loadTestData("unmarshal-table-withinline.toml")), 866 | expect: map[string]withTableArray{ 867 | "tab1": {Tabarray: []map[string]string{{"key": "1"}}}, 868 | "tab2": {Tabarray: []map[string]string{{"key": "2"}}}, 869 | }, 870 | }, 871 | 872 | // errors 873 | { 874 | data: string(loadTestData("unmarshal-table-conflict-1.toml")), 875 | err: lineError(7, fmt.Errorf("table `a' is in conflict with table in line 4")), 876 | expect: &testStruct{}, 877 | }, 878 | { 879 | data: string(loadTestData("unmarshal-table-conflict-2.toml")), 880 | err: lineError(7, fmt.Errorf("table `a.b' is in conflict with line 5")), 881 | expect: &testStruct{}, 882 | }, 883 | { 884 | data: string(loadTestData("unmarshal-table-conflict-3.toml")), 885 | err: lineError(8, fmt.Errorf("key `b' is in conflict with table in line 4")), 886 | expect: &testStruct{}, 887 | }, 888 | { 889 | data: string(loadTestData("unmarshal-table-newline-req-1.toml")), 890 | err: lineError(3, errNewlineRequired), 891 | expect: &testStruct{}, 892 | }, 893 | { 894 | data: string(loadTestData("unmarshal-table-newline-req-2.toml")), 895 | err: lineError(4, errNewlineRequired), 896 | expect: &testStruct{}, 897 | }, 898 | { 899 | data: string(loadTestData("unmarshal-table-inline-comma-invalid-1.toml")), 900 | err: lineError(3, errInlineTableCommaAtEnd), 901 | expect: &testStruct{}, 902 | }, 903 | { 904 | data: string(loadTestData("unmarshal-table-inline-comma-invalid-2.toml")), 905 | err: lineError(3, errInlineTableCommaRequired), 906 | expect: &testStruct{}, 907 | }, 908 | {`[]`, lineError(1, errParse), &testStruct{}}, 909 | {`[a.]`, lineError(1, errParse), &testStruct{}}, 910 | {`[a..b]`, lineError(1, errParse), &testStruct{}}, 911 | {`[.b]`, lineError(1, errParse), &testStruct{}}, 912 | {`[.]`, lineError(1, errParse), &testStruct{}}, 913 | {` = "no key name" # not allowed`, lineError(1, errParse), &testStruct{}}, 914 | { 915 | data: `ignored = "value"`, 916 | err: lineError(1, fmt.Errorf("field corresponding to `ignored' in toml.testIgnoredFieldStruct cannot be set through TOML")), 917 | expect: &testIgnoredFieldStruct{}, 918 | }, 919 | { 920 | data: `"-" = "value"`, 921 | err: lineError(1, fmt.Errorf("field corresponding to `-' is not defined in toml.testIgnoredFieldStruct")), 922 | expect: &testIgnoredFieldStruct{}, 923 | }, 924 | { 925 | data: `named = "value"`, 926 | err: lineError(1, fmt.Errorf("field corresponding to `named' is not defined in toml.testNamedFieldStruct")), 927 | expect: &testNamedFieldStruct{}, 928 | }, 929 | { 930 | data: ` 931 | [a] 932 | d = 2 933 | y = 3 934 | `, 935 | err: lineError(4, fmt.Errorf("field corresponding to `y' is not defined in toml.A")), 936 | expect: &testStruct{}, 937 | }, 938 | }) 939 | } 940 | 941 | func TestUnmarshal_WithEmbeddedStruct(t *testing.T) { 942 | type TestEmbStructA struct { 943 | A string 944 | } 945 | testUnmarshal(t, []testcase{ 946 | { 947 | data: `a = "value"`, 948 | expect: &struct { 949 | TestEmbStructA 950 | A string 951 | }{ 952 | A: "value", 953 | }, 954 | }, 955 | { 956 | data: `a = "value"`, 957 | expect: &struct { 958 | A string 959 | TestEmbStructA 960 | }{ 961 | A: "value", 962 | }, 963 | }, 964 | }) 965 | } 966 | 967 | func TestUnmarshal_WithArrayTable(t *testing.T) { 968 | type Product struct { 969 | Name string 970 | SKU int64 971 | Color string 972 | } 973 | type Physical struct { 974 | Color string 975 | Shape string 976 | } 977 | type Variety struct { 978 | Name string 979 | } 980 | type Fruit struct { 981 | Name string 982 | Physical Physical 983 | Variety []Variety 984 | } 985 | type testStruct struct { 986 | Products []Product 987 | Fruit []Fruit 988 | } 989 | type testStructWithMap struct { 990 | Fruit []map[string][]struct { 991 | Name string 992 | } 993 | } 994 | testUnmarshal(t, []testcase{ 995 | { 996 | data: string(loadTestData("unmarshal-arraytable.toml")), 997 | expect: &testStruct{ 998 | Products: []Product{ 999 | {Name: "Hammer", SKU: 738594937}, 1000 | {}, 1001 | {Name: "Nail", SKU: 284758393, Color: "gray"}, 1002 | }, 1003 | }, 1004 | }, 1005 | { 1006 | data: string(loadTestData("unmarshal-arraytable-inline.toml")), 1007 | expect: &testStruct{ 1008 | Products: []Product{ 1009 | {Name: "Hammer", SKU: 738594937}, 1010 | {}, 1011 | {Name: "Nail", SKU: 284758393, Color: "gray"}, 1012 | }, 1013 | }, 1014 | }, 1015 | { 1016 | data: string(loadTestData("unmarshal-arraytable-nested-1.toml")), 1017 | expect: &testStruct{ 1018 | Fruit: []Fruit{ 1019 | { 1020 | Name: "apple", 1021 | Physical: Physical{ 1022 | Color: "red", 1023 | Shape: "round", 1024 | }, 1025 | Variety: []Variety{ 1026 | {Name: "red delicious"}, 1027 | {Name: "granny smith"}, 1028 | }, 1029 | }, 1030 | { 1031 | Name: "banana", 1032 | Physical: Physical{ 1033 | Color: "yellow", 1034 | Shape: "lune", 1035 | }, 1036 | Variety: []Variety{ 1037 | {Name: "plantain"}, 1038 | }, 1039 | }, 1040 | }, 1041 | }, 1042 | }, 1043 | { 1044 | data: string(loadTestData("unmarshal-arraytable-nested-2.toml")), 1045 | expect: &testStructWithMap{ 1046 | Fruit: []map[string][]struct { 1047 | Name string 1048 | }{ 1049 | {"variety": {{Name: "red delicious"}, {Name: "granny smith"}}}, 1050 | {"variety": {{Name: "plantain"}}, "area": {{Name: "phillippines"}}}, 1051 | }, 1052 | }, 1053 | }, 1054 | { 1055 | data: string(loadTestData("unmarshal-arraytable-nested-3.toml")), 1056 | expect: &testStructWithMap{ 1057 | Fruit: []map[string][]struct { 1058 | Name string 1059 | }{ 1060 | {"variety": {{Name: "red delicious"}, {Name: "granny smith"}}}, 1061 | {"variety": {{Name: "plantain"}}, "area": {{Name: "phillippines"}}}, 1062 | }, 1063 | }, 1064 | }, 1065 | 1066 | // errors 1067 | { 1068 | data: string(loadTestData("unmarshal-arraytable-conflict-1.toml")), 1069 | err: lineError(10, fmt.Errorf("table `fruit.variety' is in conflict with array table in line 6")), 1070 | expect: &testStruct{}, 1071 | }, 1072 | { 1073 | data: string(loadTestData("unmarshal-arraytable-conflict-2.toml")), 1074 | err: lineError(10, fmt.Errorf("array table `fruit.variety' is in conflict with table in line 6")), 1075 | expect: &testStruct{}, 1076 | }, 1077 | { 1078 | data: string(loadTestData("unmarshal-arraytable-conflict-3.toml")), 1079 | err: lineError(8, fmt.Errorf("array table `fruit.variety' is in conflict with line 5")), 1080 | expect: &testStruct{}, 1081 | }, 1082 | }) 1083 | } 1084 | 1085 | type testUnmarshalerString string 1086 | 1087 | func (u *testUnmarshalerString) UnmarshalTOML(data []byte) error { 1088 | *u = testUnmarshalerString("Unmarshaled: " + string(data)) 1089 | return nil 1090 | } 1091 | 1092 | type testUnmarshalerStruct struct { 1093 | Title string 1094 | Author testUnmarshalerString 1095 | } 1096 | 1097 | func (u *testUnmarshalerStruct) UnmarshalTOML(data []byte) error { 1098 | u.Title = "Unmarshaled: " + string(data) 1099 | return nil 1100 | } 1101 | 1102 | func TestUnmarshal_WithUnmarshaler(t *testing.T) { 1103 | type testStruct struct { 1104 | Title testUnmarshalerString 1105 | MaxConn testUnmarshalerString 1106 | Ports testUnmarshalerString 1107 | Servers testUnmarshalerString 1108 | Table testUnmarshalerString 1109 | Arraytable testUnmarshalerString 1110 | InlineTable testUnmarshalerString 1111 | ArrayOfStruct []testUnmarshalerStruct 1112 | } 1113 | data := loadTestData("unmarshal-unmarshaler.toml") 1114 | var v testStruct 1115 | if err := Unmarshal(data, &v); err != nil { 1116 | t.Fatal(err) 1117 | } 1118 | actual := v 1119 | expect := testStruct{ 1120 | Title: `Unmarshaled: "testtitle"`, 1121 | MaxConn: `Unmarshaled: 777`, 1122 | Ports: `Unmarshaled: [8080, 8081, 8082]`, 1123 | Servers: `Unmarshaled: [1, 2, 3]`, 1124 | InlineTable: `Unmarshaled: {name = "alice"}`, 1125 | Table: "Unmarshaled: [table]\nname = \"alice\"", 1126 | Arraytable: "Unmarshaled: [[arraytable]]\nname = \"alice\"\n[[arraytable]]\nname = \"bob\"", 1127 | ArrayOfStruct: []testUnmarshalerStruct{{ 1128 | Title: "Unmarshaled: [[array_of_struct]]\ntitle = \"Alice's Adventures in Wonderland\"\nauthor = \"Lewis Carroll\"", 1129 | Author: "", 1130 | }}, 1131 | } 1132 | if !reflect.DeepEqual(actual, expect) { 1133 | t.Error("diff:", pretty.Compare(actual, expect)) 1134 | } 1135 | } 1136 | 1137 | func TestUnmarshal_WithUnmarshalerForTopLevelStruct(t *testing.T) { 1138 | data := `title = "Alice's Adventures in Wonderland" 1139 | author = "Lewis Carroll" 1140 | ` 1141 | var v testUnmarshalerStruct 1142 | if err := Unmarshal([]byte(data), &v); err != nil { 1143 | t.Fatal(err) 1144 | } 1145 | actual := v 1146 | expect := testUnmarshalerStruct{ 1147 | Title: `Unmarshaled: title = "Alice's Adventures in Wonderland" 1148 | author = "Lewis Carroll" 1149 | `, 1150 | Author: "", 1151 | } 1152 | if !reflect.DeepEqual(actual, expect) { 1153 | t.Errorf(`toml.Unmarshal(data, &v); v => %#v; want %#v`, actual, expect) 1154 | } 1155 | } 1156 | 1157 | type testTextUnmarshaler string 1158 | 1159 | var errTextUnmarshaler = errors.New("UnmarshalText called with data = error") 1160 | 1161 | func (x *testTextUnmarshaler) UnmarshalText(data []byte) error { 1162 | *x = testTextUnmarshaler("Unmarshaled: " + string(data)) 1163 | if string(data) == "error" { 1164 | return errTextUnmarshaler 1165 | } 1166 | return nil 1167 | } 1168 | 1169 | func TestUnmarshal_WithTextUnmarshaler(t *testing.T) { 1170 | type testStruct struct { 1171 | Str testTextUnmarshaler 1172 | Int testTextUnmarshaler 1173 | Float testTextUnmarshaler 1174 | Arraytable []testStruct 1175 | BigInt *big.Int 1176 | } 1177 | 1178 | tests := []testcase{ 1179 | { 1180 | data: string(loadTestData("unmarshal-textunmarshaler.toml")), 1181 | expect: &testStruct{ 1182 | Str: "Unmarshaled: str", 1183 | Int: "Unmarshaled: 11", 1184 | Float: "Unmarshaled: 12.0", 1185 | Arraytable: []testStruct{{Str: "Unmarshaled: str2", Int: "Unmarshaled: 22", Float: "Unmarshaled: 23.0"}}, 1186 | }, 1187 | }, 1188 | { 1189 | data: `str = "error"`, 1190 | expect: &testStruct{Str: "Unmarshaled: error"}, 1191 | err: lineErrorField(1, "toml.testStruct.Str", errTextUnmarshaler), 1192 | }, 1193 | // big.Int tests 1194 | { 1195 | data: `BigInt = 20`, 1196 | expect: &testStruct{BigInt: big.NewInt(20)}, 1197 | }, 1198 | { 1199 | data: `BigInt = -20`, 1200 | expect: &testStruct{BigInt: big.NewInt(-20)}, 1201 | }, 1202 | { 1203 | data: `BigInt = 0xFFFFFFFFFFFF`, 1204 | expect: &testStruct{BigInt: big.NewInt(0xFFFFFFFFFFFF)}, 1205 | }, 1206 | { 1207 | data: `BigInt = 0xFF_FF_FF_FF_FF_FF`, 1208 | expect: &testStruct{BigInt: big.NewInt(0xFFFFFFFFFFFF)}, 1209 | }, 1210 | } 1211 | testUnmarshal(t, tests) 1212 | } 1213 | 1214 | type testUnmarshalerRecString string 1215 | 1216 | func (u *testUnmarshalerRecString) UnmarshalTOML(fn func(interface{}) error) error { 1217 | var s string 1218 | if err := fn(&s); err != nil { 1219 | return err 1220 | } 1221 | *u = testUnmarshalerRecString("Unmarshaled: " + s) 1222 | return nil 1223 | } 1224 | 1225 | type testUnmarshalerRecStruct struct { 1226 | a, b int 1227 | } 1228 | 1229 | func (u *testUnmarshalerRecStruct) UnmarshalTOML(fn func(interface{}) error) error { 1230 | var uu struct{ A, B int } 1231 | if err := fn(&uu); err != nil { 1232 | return err 1233 | } 1234 | u.a, u.b = uu.A, uu.B 1235 | return nil 1236 | } 1237 | 1238 | func TestUnmarshal_WithUnmarshalerRec(t *testing.T) { 1239 | type testStruct struct { 1240 | String testUnmarshalerRecString 1241 | Struct testUnmarshalerRecStruct 1242 | Arraytable []testStruct 1243 | } 1244 | var v testStruct 1245 | err := Unmarshal(loadTestData("unmarshal-unmarshalerrec.toml"), &v) 1246 | if err != nil { 1247 | t.Fatal("Unexpected error:", err) 1248 | } 1249 | expect := testStruct{ 1250 | String: "Unmarshaled: str1", 1251 | Struct: testUnmarshalerRecStruct{a: 1, b: 2}, 1252 | Arraytable: []testStruct{ 1253 | { 1254 | String: "Unmarshaled: str2", 1255 | Struct: testUnmarshalerRecStruct{a: 3, b: 4}, 1256 | }, 1257 | }, 1258 | } 1259 | if !reflect.DeepEqual(v, expect) { 1260 | t.Errorf(`toml.Unmarshal(data, &v); v => %#v; want %#v`, v, expect) 1261 | } 1262 | } 1263 | 1264 | func TestUnmarshal_WithMultibyteString(t *testing.T) { 1265 | type testStruct struct { 1266 | Name string 1267 | Numbers []string 1268 | } 1269 | v := testStruct{} 1270 | data := `name = "七一〇七" 1271 | numbers = ["壱", "弐", "参"] 1272 | ` 1273 | if err := Unmarshal([]byte(data), &v); err != nil { 1274 | t.Fatal(err) 1275 | } 1276 | actual := v 1277 | expect := testStruct{ 1278 | Name: "七一〇七", 1279 | Numbers: []string{"壱", "弐", "参"}, 1280 | } 1281 | if !reflect.DeepEqual(actual, expect) { 1282 | t.Errorf(`toml.Unmarshal([]byte(data), &v); v => %#v; want %#v`, actual, expect) 1283 | } 1284 | } 1285 | 1286 | func TestUnmarshal_WithPointers(t *testing.T) { 1287 | type Inline struct { 1288 | Key1 string 1289 | Key2 *string 1290 | Key3 **string 1291 | } 1292 | type Table struct { 1293 | Key1 *string 1294 | Key2 **string 1295 | Key3 ***string 1296 | } 1297 | type testStruct struct { 1298 | Inline *Inline 1299 | Tables []*Table 1300 | } 1301 | type testStruct2 struct { 1302 | Inline **Inline 1303 | Tables []**Table 1304 | } 1305 | type testStruct3 struct { 1306 | Inline ***Inline 1307 | Tables []***Table 1308 | } 1309 | s1 := "a" 1310 | s2 := &s1 1311 | s3 := &s2 1312 | s4 := &s3 1313 | s5 := "b" 1314 | s6 := &s5 1315 | s7 := &s6 1316 | s8 := &s7 1317 | i1 := &Inline{"test", s2, s7} 1318 | i2 := &i1 1319 | i3 := &i2 1320 | t1 := &Table{s2, s3, s4} 1321 | t2 := &Table{s6, s7, s8} 1322 | t3 := &t1 1323 | t4 := &t2 1324 | sc := &testStruct{ 1325 | Inline: i1, Tables: []*Table{t1, t2}, 1326 | } 1327 | data := string(loadTestData("unmarshal-pointer.toml")) 1328 | testUnmarshal(t, []testcase{ 1329 | {data, nil, sc}, 1330 | {data, nil, &testStruct2{ 1331 | Inline: i2, 1332 | Tables: []**Table{&t1, &t2}, 1333 | }}, 1334 | {data, nil, &testStruct3{ 1335 | Inline: i3, 1336 | Tables: []***Table{&t3, &t4}, 1337 | }}, 1338 | }) 1339 | } 1340 | 1341 | // This test checks that maps can be unmarshaled into directly. 1342 | func TestUnmarshalMap(t *testing.T) { 1343 | testUnmarshal(t, []testcase{ 1344 | { 1345 | data: ` 1346 | name = "evan" 1347 | foo = 1 1348 | `, 1349 | expect: map[string]interface{}{"name": "evan", "foo": int64(1)}, 1350 | }, 1351 | { 1352 | data: `[""] 1353 | a = 1 1354 | `, 1355 | expect: map[string]interface{}{"": map[string]interface{}{"a": int64(1)}}, 1356 | }, 1357 | { 1358 | data: `["table"] 1359 | a = 1 1360 | `, 1361 | expect: map[string]interface{}{"table": map[string]interface{}{"a": int64(1)}}, 1362 | }, 1363 | { 1364 | data: `["\u2222"] 1365 | a = 1 1366 | `, 1367 | expect: map[string]interface{}{"\u2222": map[string]interface{}{"a": int64(1)}}, 1368 | }, 1369 | { 1370 | data: `[p] 1371 | first = "evan" 1372 | `, 1373 | expect: map[string]*Name{"p": {First: "evan"}}, 1374 | }, 1375 | { 1376 | data: `foo = 1 1377 | bar = 2 1378 | `, 1379 | expect: map[testTextUnmarshaler]int{"Unmarshaled: foo": 1, "Unmarshaled: bar": 2}, 1380 | }, 1381 | { 1382 | data: `"foo" = 1 1383 | "foo.bar" = 2 1384 | `, 1385 | expect: map[testTextUnmarshaler]int{"Unmarshaled: foo": 1, "Unmarshaled: foo.bar": 2}, 1386 | }, 1387 | 1388 | { 1389 | data: `1 = 1 1390 | -2 = 2 1391 | `, 1392 | expect: map[int]int{1: 1, -2: 2}, 1393 | }, 1394 | { 1395 | data: `1 = 1 1396 | -129 = 2 1397 | `, 1398 | expect: map[int8]int{1: 1}, 1399 | err: lineError(2, &overflowError{reflect.Int8, "-129"}), 1400 | }, 1401 | }) 1402 | } 1403 | 1404 | func TestUnmarshal_WithQuotedKeyValue(t *testing.T) { 1405 | type nestedStruct struct { 1406 | Truthy bool 1407 | } 1408 | type testStruct struct { 1409 | Table map[string]nestedStruct 1410 | } 1411 | 1412 | testUnmarshal(t, []testcase{ 1413 | {data: `"a" = 1`, expect: map[string]int{"a": 1}}, 1414 | {data: `"a.b" = 1`, expect: map[string]int{"a.b": 1}}, 1415 | {data: `"\u2222" = 1`, expect: map[string]int{"\u2222": 1}}, 1416 | {data: `"\"" = 1`, expect: map[string]int{"\"": 1}}, 1417 | {data: `"" = 1`, expect: map[string]int{"": 1}}, 1418 | {data: `'a' = 1`, expect: map[string]int{}, err: lineError(1, errParse)}, 1419 | // Inline tables: 1420 | { 1421 | data: ` 1422 | [table] 1423 | "some.key" = {truthy = true} 1424 | `, 1425 | expect: &testStruct{Table: map[string]nestedStruct{ 1426 | "some.key": {Truthy: true}, 1427 | }}, 1428 | }, 1429 | { 1430 | data: ` 1431 | "some.key" = [{truthy = true}] 1432 | `, 1433 | expect: map[string][]nestedStruct{ 1434 | "some.key": {{Truthy: true}}, 1435 | }, 1436 | }, 1437 | }) 1438 | } 1439 | 1440 | func TestUnmarshal_WithCustomPrimitiveType(t *testing.T) { 1441 | type ( 1442 | String string 1443 | Int int 1444 | Bool bool 1445 | ) 1446 | type X struct { 1447 | S String 1448 | I Int 1449 | B Bool 1450 | } 1451 | 1452 | input := ` 1453 | s = "string" 1454 | i = 1 1455 | b = true 1456 | ` 1457 | testUnmarshal(t, []testcase{ 1458 | {input, nil, &X{"string", 1, true}}, 1459 | }) 1460 | } 1461 | 1462 | func TestUnmarshal_WithInterface(t *testing.T) { 1463 | var exp interface{} = map[string]interface{}{ 1464 | "string": "string", 1465 | "int": int64(3), 1466 | "float": float64(4), 1467 | "boolean": true, 1468 | "datetime": mustTime(time.Parse(time.RFC3339Nano, "1979-05-27T00:32:00.999999-07:00")), 1469 | "array": []interface{}{int64(1), int64(2), int64(3)}, 1470 | "inline": map[string]interface{}{"key": "value"}, 1471 | "table": map[string]interface{}{"key": "value"}, 1472 | "arraytable": []interface{}{ 1473 | map[string]interface{}{"key": "value"}, 1474 | map[string]interface{}{"key": "value"}, 1475 | }, 1476 | } 1477 | 1478 | type nonemptyIf interface { 1479 | method() 1480 | } 1481 | nonemptyIfType := reflect.TypeOf((*nonemptyIf)(nil)).Elem() 1482 | 1483 | data := string(loadTestData("unmarshal-interface.toml")) 1484 | testUnmarshal(t, []testcase{ 1485 | {data, nil, &exp}, 1486 | // can't unmarshal into non-empty interface{} 1487 | {`v = "string"`, lineError(1, &unmarshalTypeError{"string", "", nonemptyIfType}), map[string]nonemptyIf{}}, 1488 | {`v = 1`, lineError(1, &unmarshalTypeError{"integer", "", nonemptyIfType}), map[string]nonemptyIf{}}, 1489 | {`v = 1.0`, lineError(1, &unmarshalTypeError{"float", "", nonemptyIfType}), map[string]nonemptyIf{}}, 1490 | {`v = true`, lineError(1, &unmarshalTypeError{"boolean", "", nonemptyIfType}), map[string]nonemptyIf{}}, 1491 | {`v = [1, 2]`, lineError(1, &unmarshalTypeError{"array", "slice", nonemptyIfType}), map[string]nonemptyIf{}}, 1492 | {`[v]`, lineError(1, &unmarshalTypeError{"table", "struct or map", nonemptyIfType}), map[string]nonemptyIf{}}, 1493 | {`[[v]]`, lineError(1, &unmarshalTypeError{"array table", "slice", nonemptyIfType}), map[string]nonemptyIf{}}, 1494 | }) 1495 | } 1496 | 1497 | // This test checks that error line numbers are correct for both 1498 | // kinds of line-endings. 1499 | func TestUnmarshal_ErrorLine(t *testing.T) { 1500 | testUnmarshal(t, []testcase{ 1501 | { 1502 | data: string(loadTestData("unmarshal-errline-lf.toml")), 1503 | err: lineError(5, fmt.Errorf("key `key2' is in conflict with line 3")), 1504 | expect: map[string]interface{}{}, 1505 | }, 1506 | { 1507 | data: string(loadTestData("unmarshal-errline-crlf.toml")), 1508 | err: lineError(5, fmt.Errorf("key `key2' is in conflict with line 3")), 1509 | expect: map[string]interface{}{}, 1510 | }, 1511 | }) 1512 | } 1513 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "bytes" 5 | "encoding" 6 | "fmt" 7 | "io" 8 | "math" 9 | "reflect" 10 | "sort" 11 | "strconv" 12 | "time" 13 | 14 | "github.com/naoina/toml/ast" 15 | ) 16 | 17 | const ( 18 | tagOmitempty = "omitempty" 19 | tagSkip = "-" 20 | ) 21 | 22 | // Marshal returns the TOML encoding of v. 23 | // 24 | // Struct values encode as TOML. Each exported struct field becomes a field of 25 | // the TOML structure unless 26 | // - the field's tag is "-", or 27 | // - the field is empty and its tag specifies the "omitempty" option. 28 | // 29 | // The "toml" key in the struct field's tag value is the key name, followed by 30 | // an optional comma and options. Examples: 31 | // 32 | // // Field is ignored by this package. 33 | // Field int `toml:"-"` 34 | // 35 | // // Field appears in TOML as key "myName". 36 | // Field int `toml:"myName"` 37 | // 38 | // // Field appears in TOML as key "myName" and the field is omitted from the 39 | // // result of encoding if its value is empty. 40 | // Field int `toml:"myName,omitempty"` 41 | // 42 | // // Field appears in TOML as key "field", but the field is skipped if 43 | // // empty. Note the leading comma. 44 | // Field int `toml:",omitempty"` 45 | func (cfg *Config) Marshal(v interface{}) ([]byte, error) { 46 | buf := new(bytes.Buffer) 47 | err := cfg.NewEncoder(buf).Encode(v) 48 | return buf.Bytes(), err 49 | } 50 | 51 | // A Encoder writes TOML to an output stream. 52 | type Encoder struct { 53 | w io.Writer 54 | cfg *Config 55 | } 56 | 57 | // NewEncoder returns a new Encoder that writes to w. 58 | func (cfg *Config) NewEncoder(w io.Writer) *Encoder { 59 | return &Encoder{w, cfg} 60 | } 61 | 62 | // Encode writes the TOML of v to the stream. 63 | // See the documentation for Marshal for details about the conversion of Go values to TOML. 64 | func (e *Encoder) Encode(v interface{}) error { 65 | var ( 66 | buf = &tableBuf{typ: ast.TableTypeNormal} 67 | rv = reflect.ValueOf(v) 68 | err error 69 | ) 70 | 71 | for rv.Kind() == reflect.Ptr { 72 | if rv.IsNil() { 73 | return &marshalNilError{rv.Type()} 74 | } 75 | rv = rv.Elem() 76 | } 77 | 78 | switch rv.Kind() { 79 | case reflect.Struct: 80 | _, err = buf.structFields(e.cfg, rv) 81 | case reflect.Map: 82 | _, err = buf.mapFields(e.cfg, rv) 83 | case reflect.Interface: 84 | return e.Encode(rv.Interface()) 85 | default: 86 | err = &marshalTableError{rv.Type()} 87 | } 88 | if err != nil { 89 | return err 90 | } 91 | return buf.writeTo(e.w, "") 92 | } 93 | 94 | // Marshaler can be implemented to override the encoding of TOML values. The returned text 95 | // must be a simple TOML value (i.e. not a table) and is inserted into marshaler output. 96 | // 97 | // This interface exists for backwards-compatibility reasons. You probably want to 98 | // implement encoding.TextMarshaler or MarshalerRec instead. 99 | type Marshaler interface { 100 | MarshalTOML() ([]byte, error) 101 | } 102 | 103 | // MarshalerRec can be implemented to override the TOML encoding of a type. 104 | // The returned value is marshaled in place of the receiver. 105 | type MarshalerRec interface { 106 | MarshalTOML() (interface{}, error) 107 | } 108 | 109 | type tableBuf struct { 110 | name string // already escaped / quoted 111 | typ ast.TableType 112 | 113 | body []byte // text below table header 114 | children []*tableBuf // sub-tables of this table 115 | 116 | arrayDepth int // if > 0 in value(x), x is contained in an array. 117 | mixedArrayDepth int // if > 0 in value(x), x is contained in a mixed array. 118 | } 119 | 120 | // writeTo writes b and all of its children to w. 121 | func (b *tableBuf) writeTo(w io.Writer, prefix string) error { 122 | key := b.name // TODO: escape dots 123 | if prefix != "" { 124 | key = prefix + "." + key 125 | } 126 | 127 | if b.name != "" { 128 | head := "[" + key + "]" 129 | if b.typ == ast.TableTypeArray { 130 | head = "[" + head + "]" 131 | } 132 | head += "\n" 133 | if _, err := io.WriteString(w, head); err != nil { 134 | return err 135 | } 136 | } 137 | if _, err := w.Write(b.body); err != nil { 138 | return err 139 | } 140 | 141 | for i, child := range b.children { 142 | if len(b.body) > 0 || i > 0 { 143 | if _, err := w.Write([]byte("\n")); err != nil { 144 | return err 145 | } 146 | } 147 | if err := child.writeTo(w, key); err != nil { 148 | return err 149 | } 150 | } 151 | return nil 152 | } 153 | 154 | // newChild creates a new child table of b. 155 | func (b *tableBuf) newChild(name string) *tableBuf { 156 | child := &tableBuf{name: quoteName(name), typ: ast.TableTypeNormal} 157 | if b.arrayDepth > 0 { 158 | child.typ = ast.TableTypeArray 159 | // Note: arrayDepth does not inherit into child tables! 160 | } 161 | if b.mixedArrayDepth > 0 { 162 | child.typ = ast.TableTypeInline 163 | child.mixedArrayDepth = b.mixedArrayDepth 164 | b.body = append(b.body, '{') 165 | } 166 | return child 167 | } 168 | 169 | // addChild adds a child table to b. 170 | // This is called after all values in child have already been 171 | // written to child.body. 172 | func (b *tableBuf) addChild(cfg *Config, child *tableBuf) { 173 | // Inline tables are not tracked in b.children. 174 | if child.typ == ast.TableTypeInline { 175 | b.body = append(b.body, child.body...) 176 | b.body = append(b.body, '}') 177 | return 178 | } 179 | 180 | // Empty table elision: we can avoid writing a table that doesn't have any keys on its 181 | // own. Array tables can't be elided because they define array elements (which would 182 | // be missing if elided). 183 | if len(child.body) == 0 && child.typ == ast.TableTypeNormal && !cfg.WriteEmptyTables { 184 | for _, gchild := range child.children { 185 | gchild.name = child.name + "." + gchild.name 186 | b.addChild(cfg, gchild) 187 | } 188 | return 189 | } 190 | b.children = append(b.children, child) 191 | } 192 | 193 | // structFields writes applicable fields of a struct. 194 | func (b *tableBuf) structFields(cfg *Config, rv reflect.Value) (newTables []*tableBuf, err error) { 195 | rt := rv.Type() 196 | for i := 0; i < rv.NumField(); i++ { 197 | // Check if the field should be written at all. 198 | ft := rt.Field(i) 199 | if ft.PkgPath != "" && !ft.Anonymous { // not exported 200 | continue 201 | } 202 | name, rest := extractTag(ft.Tag.Get(fieldTagName)) 203 | if name == tagSkip { 204 | continue 205 | } 206 | fv := rv.Field(i) 207 | if rest == tagOmitempty && isEmptyValue(fv) { 208 | continue 209 | } 210 | if name == "" { 211 | name = cfg.FieldToKey(rt, ft.Name) 212 | } 213 | 214 | // If the current table is inline, write separators. 215 | if b.typ == ast.TableTypeInline && i > 0 { 216 | b.body = append(b.body, ", "...) 217 | } 218 | // Write the key/value pair. 219 | tables, err := b.field(cfg, name, fv) 220 | if err != nil { 221 | return newTables, err 222 | } 223 | newTables = append(newTables, tables...) 224 | } 225 | return newTables, nil 226 | } 227 | 228 | // mapFields writes the content of a map. 229 | func (b *tableBuf) mapFields(cfg *Config, rv reflect.Value) ([]*tableBuf, error) { 230 | // Marshal and sort the keys first. 231 | var keys = rv.MapKeys() 232 | var keylist = make(mapKeyList, len(keys)) 233 | for i, key := range keys { 234 | var err error 235 | keylist[i].key, err = encodeMapKey(key) 236 | if err != nil { 237 | return nil, err 238 | } 239 | keylist[i].value = rv.MapIndex(key) 240 | } 241 | sort.Sort(keylist) 242 | 243 | var newTables []*tableBuf 244 | var index int 245 | for _, kv := range keylist { 246 | // If the current table is inline, add separators. 247 | if b.typ == ast.TableTypeInline && index > 0 { 248 | b.body = append(b.body, ", "...) 249 | } 250 | // Write the key/value pair. 251 | tables, err := b.field(cfg, kv.key, kv.value) 252 | if err != nil { 253 | return newTables, err 254 | } 255 | newTables = append(newTables, tables...) 256 | index++ 257 | } 258 | return newTables, nil 259 | } 260 | 261 | // field writes a key/value pair. 262 | func (b *tableBuf) field(cfg *Config, name string, rv reflect.Value) ([]*tableBuf, error) { 263 | off := len(b.body) 264 | b.body = append(b.body, quoteName(name)...) 265 | b.body = append(b.body, " = "...) 266 | tables, err := b.value(cfg, rv, name) 267 | switch { 268 | case b.typ == ast.TableTypeInline: 269 | // Inline tables don't have newlines. 270 | case len(tables) > 0: 271 | // Value was written as a new table, rub out "key =". 272 | b.body = b.body[:off] 273 | default: 274 | // Regular key/value pair in table. 275 | b.body = append(b.body, '\n') 276 | } 277 | return tables, err 278 | } 279 | 280 | // value writes a plain value. 281 | func (b *tableBuf) value(cfg *Config, rv reflect.Value, name string) ([]*tableBuf, error) { 282 | isMarshaler, tables, err := b.marshaler(cfg, rv, name) 283 | if isMarshaler { 284 | return tables, err 285 | } 286 | 287 | k := rv.Kind() 288 | switch { 289 | case k >= reflect.Int && k <= reflect.Int64: 290 | b.body = strconv.AppendInt(b.body, rv.Int(), 10) 291 | return nil, nil 292 | 293 | case k >= reflect.Uint && k <= reflect.Uintptr: 294 | b.body = strconv.AppendUint(b.body, rv.Uint(), 10) 295 | return nil, nil 296 | 297 | case k >= reflect.Float32 && k <= reflect.Float64: 298 | b.body = appendFloat(b.body, rv.Float()) 299 | return nil, nil 300 | 301 | case k == reflect.Bool: 302 | b.body = strconv.AppendBool(b.body, rv.Bool()) 303 | return nil, nil 304 | 305 | case k == reflect.String: 306 | b.body = strconv.AppendQuote(b.body, rv.String()) 307 | return nil, nil 308 | 309 | case k == reflect.Ptr || k == reflect.Interface: 310 | if rv.IsNil() { 311 | return nil, &marshalNilError{rv.Type()} 312 | } 313 | return b.value(cfg, rv.Elem(), name) 314 | 315 | case k == reflect.Slice || k == reflect.Array: 316 | return b.array(cfg, rv, name) 317 | 318 | case k == reflect.Struct: 319 | child := b.newChild(name) 320 | tables, err := child.structFields(cfg, rv) 321 | b.addChild(cfg, child) 322 | if child.typ == ast.TableTypeInline { 323 | return nil, err 324 | } 325 | tables = append(tables, child) 326 | return tables, err 327 | 328 | case k == reflect.Map: 329 | child := b.newChild(name) 330 | tables, err := child.mapFields(cfg, rv) 331 | b.addChild(cfg, child) 332 | if child.typ == ast.TableTypeInline { 333 | return nil, err 334 | } 335 | tables = append(tables, child) 336 | return tables, err 337 | 338 | default: 339 | return nil, fmt.Errorf("toml: marshal: unsupported type %v", rv.Kind()) 340 | } 341 | } 342 | 343 | func (b *tableBuf) array(cfg *Config, rv reflect.Value, name string) ([]*tableBuf, error) { 344 | rvlen := rv.Len() 345 | if rvlen == 0 { 346 | b.body = append(b.body, '[', ']') 347 | return nil, nil 348 | } 349 | 350 | // If any parent value is a mixed array, this array must also be 351 | // written as a mixed array. 352 | if b.mixedArrayDepth > 0 { 353 | err := b.mixedArray(cfg, rv, name) 354 | return nil, err 355 | } 356 | 357 | // Bump depth. This ensures that any tables created while 358 | // encoding the array will become array tables. 359 | b.arrayDepth++ 360 | defer func() { b.arrayDepth-- }() 361 | 362 | // Take a snapshot of the current state. 363 | var ( 364 | childrenBeforeArray = b.children 365 | offsetBeforeArray = len(b.body) 366 | ) 367 | 368 | var ( 369 | newTables []*tableBuf 370 | anyPlainValue = false // true if any non-table was written. 371 | ) 372 | b.body = append(b.body, '[') 373 | for i := 0; i < rvlen; i++ { 374 | if i > 0 { 375 | b.body = append(b.body, ", "...) 376 | } 377 | 378 | tables, err := b.value(cfg, rv.Index(i), name) 379 | if err != nil { 380 | return newTables, err 381 | } 382 | if len(tables) == 0 { 383 | anyPlainValue = true 384 | } 385 | newTables = append(newTables, tables...) 386 | 387 | if len(newTables) > 0 && (anyPlainValue || b.arrayDepth > 1) { 388 | // Turns out this is a heterogenous array, mixing table and non-table values, 389 | // or a multi-dimensional array containing tables. If any tables were already 390 | // created, we need to remove them again and start over. 391 | b.children = childrenBeforeArray 392 | b.body = b.body[:offsetBeforeArray] 393 | err := b.mixedArray(cfg, rv, name) 394 | return nil, err 395 | } 396 | } 397 | 398 | if anyPlainValue { 399 | b.body = append(b.body, ']') 400 | } else { 401 | // The array contained only tables, rub out the initial '[' 402 | // to reset the buffer. 403 | b.body = b.body[:offsetBeforeArray] 404 | } 405 | return newTables, nil 406 | } 407 | 408 | // mixedArray writes rv as an array of mixed table / non-table values. 409 | // When this is called, we already know that rv is non-empty. 410 | func (b *tableBuf) mixedArray(cfg *Config, rv reflect.Value, name string) error { 411 | // Ensure that any elements written as tables are written inline. 412 | b.mixedArrayDepth++ 413 | defer func() { b.mixedArrayDepth-- }() 414 | 415 | b.body = append(b.body, '[') 416 | defer func() { b.body = append(b.body, ']') }() 417 | 418 | for i := 0; i < rv.Len(); i++ { 419 | if i > 0 { 420 | b.body = append(b.body, ", "...) 421 | } 422 | tables, err := b.value(cfg, rv.Index(i), name) 423 | if len(tables) > 0 { 424 | panic("toml: b.value created new tables in inline-table mode") 425 | } 426 | if err != nil { 427 | return err 428 | } 429 | } 430 | return nil 431 | } 432 | 433 | // marshaler writes a value that implements any of the marshaler interfaces. 434 | func (b *tableBuf) marshaler(cfg *Config, rv reflect.Value, name string) (handled bool, newTables []*tableBuf, err error) { 435 | switch t := rv.Interface().(type) { 436 | case encoding.TextMarshaler: 437 | enc, err := t.MarshalText() 438 | if err != nil { 439 | return true, nil, err 440 | } 441 | b.body = encodeTextMarshaler(b.body, string(enc)) 442 | return true, nil, nil 443 | case MarshalerRec: 444 | newval, err := t.MarshalTOML() 445 | if err != nil { 446 | return true, nil, err 447 | } 448 | newTables, err = b.value(cfg, reflect.ValueOf(newval), name) 449 | return true, newTables, err 450 | case Marshaler: 451 | enc, err := t.MarshalTOML() 452 | if err != nil { 453 | return true, nil, err 454 | } 455 | b.body = append(b.body, enc...) 456 | return true, nil, nil 457 | } 458 | return false, nil, nil 459 | } 460 | 461 | func encodeTextMarshaler(buf []byte, v string) []byte { 462 | // Emit the value without quotes if possible. 463 | if v == "true" || v == "false" { 464 | return append(buf, v...) 465 | } else if _, err := time.Parse(time.RFC3339Nano, v); err == nil { 466 | return append(buf, v...) 467 | } else if _, err := strconv.ParseInt(v, 10, 64); err == nil { 468 | return append(buf, v...) 469 | } else if _, err := strconv.ParseUint(v, 10, 64); err == nil { 470 | return append(buf, v...) 471 | } else if _, err := strconv.ParseFloat(v, 64); err == nil { 472 | return append(buf, v...) 473 | } 474 | return strconv.AppendQuote(buf, v) 475 | } 476 | 477 | func encodeMapKey(rv reflect.Value) (string, error) { 478 | if rv.Kind() == reflect.String { 479 | return rv.String(), nil 480 | } 481 | if tm, ok := rv.Interface().(encoding.TextMarshaler); ok { 482 | b, err := tm.MarshalText() 483 | return string(b), err 484 | } 485 | switch rv.Kind() { 486 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 487 | return strconv.FormatInt(rv.Int(), 10), nil 488 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 489 | return strconv.FormatUint(rv.Uint(), 10), nil 490 | } 491 | return "", fmt.Errorf("toml: invalid map key type %v", rv.Type()) 492 | } 493 | 494 | func isEmptyValue(v reflect.Value) bool { 495 | switch v.Kind() { 496 | case reflect.Array: 497 | // encoding/json treats all arrays with non-zero length as non-empty. We check the 498 | // array content here because zero-length arrays are almost never used. 499 | len := v.Len() 500 | for i := 0; i < len; i++ { 501 | if !isEmptyValue(v.Index(i)) { 502 | return false 503 | } 504 | } 505 | return true 506 | case reflect.Map, reflect.Slice, reflect.String: 507 | return v.Len() == 0 508 | case reflect.Bool: 509 | return !v.Bool() 510 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 511 | return v.Int() == 0 512 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 513 | return v.Uint() == 0 514 | case reflect.Float32, reflect.Float64: 515 | return v.Float() == 0 516 | case reflect.Interface, reflect.Ptr: 517 | return v.IsNil() 518 | } 519 | return false 520 | } 521 | 522 | func appendFloat(out []byte, v float64) []byte { 523 | if math.IsNaN(v) { 524 | return append(out, "nan"...) 525 | } 526 | if math.IsInf(v, -1) { 527 | return append(out, "-inf"...) 528 | } 529 | if math.IsInf(v, 1) { 530 | return append(out, "inf"...) 531 | } 532 | return strconv.AppendFloat(out, v, 'e', -1, 64) 533 | } 534 | 535 | func quoteName(s string) string { 536 | if len(s) == 0 { 537 | return strconv.Quote(s) 538 | } 539 | for _, r := range s { 540 | if r >= '0' && r <= '9' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r == '-' || r == '_' { 541 | continue 542 | } 543 | return strconv.Quote(s) 544 | } 545 | return s 546 | } 547 | 548 | type mapKeyList []struct { 549 | key string 550 | value reflect.Value 551 | } 552 | 553 | func (l mapKeyList) Len() int { return len(l) } 554 | func (l mapKeyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 555 | func (l mapKeyList) Less(i, j int) bool { return l[i].key < l[j].key } 556 | -------------------------------------------------------------------------------- /encode_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "bytes" 5 | "math" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/kylelemons/godebug/diff" 11 | "github.com/kylelemons/godebug/pretty" 12 | ) 13 | 14 | func init() { 15 | // These settings make 'mismatch' output look nicer. 16 | pretty.DefaultConfig.ShortList = 80 17 | pretty.CompareConfig.ShortList = 80 18 | } 19 | 20 | type testMarshaler struct{ S string } 21 | 22 | type testMarshalerPtr struct{ S string } 23 | 24 | func (t testMarshaler) MarshalTOML() ([]byte, error) { 25 | return []byte(t.S), nil 26 | } 27 | 28 | func (t *testMarshalerPtr) MarshalTOML() ([]byte, error) { 29 | return []byte(t.S), nil 30 | } 31 | 32 | type testMarshalerRec struct{ replacement interface{} } 33 | 34 | type testMarshalerRecPtr struct{ replacement interface{} } 35 | 36 | func (t testMarshalerRec) MarshalTOML() (interface{}, error) { 37 | return t.replacement, nil 38 | } 39 | 40 | func (t *testMarshalerRecPtr) MarshalTOML() (interface{}, error) { 41 | return t.replacement, nil 42 | } 43 | 44 | var marshalTests = []struct { 45 | v interface{} 46 | expect []byte 47 | }{ 48 | // single string: 49 | { 50 | v: struct{ Name string }{"alice"}, 51 | expect: []byte("name = \"alice\"\n"), 52 | }, 53 | // single int: 54 | { 55 | v: struct{ Age int }{7}, 56 | expect: []byte("age = 7\n"), 57 | }, 58 | // floats: 59 | { 60 | v: struct{ F float64 }{32}, 61 | expect: []byte("f = 3.2e+01\n"), // TODO: this is kind of ugly 62 | }, 63 | { 64 | v: struct{ F float64 }{-33000000.456789}, 65 | expect: []byte("f = -3.3000000456789e+07\n"), 66 | }, 67 | { 68 | v: struct{ F float64 }{math.NaN()}, 69 | expect: []byte("f = nan\n"), 70 | }, 71 | { 72 | v: struct{ F float64 }{math.Inf(1)}, 73 | expect: []byte("f = inf\n"), 74 | }, 75 | { 76 | v: struct{ F float64 }{math.Inf(-1)}, 77 | expect: []byte("f = -inf\n"), 78 | }, 79 | // multiple fields: 80 | { 81 | v: struct { 82 | Name string 83 | Age int 84 | }{"alice", 7}, 85 | expect: []byte("name = \"alice\"\nage = 7\n"), 86 | }, 87 | // ignored fields: 88 | { 89 | v: struct { 90 | Name string `toml:"-"` 91 | Age int 92 | }{"alice", 7}, 93 | expect: []byte("age = 7\n"), 94 | }, 95 | // field name override: 96 | { 97 | v: struct { 98 | Name string `toml:"my_name"` 99 | }{"bob"}, 100 | expect: []byte("my_name = \"bob\"\n"), 101 | }, 102 | { 103 | v: struct { 104 | Name string `toml:"my_name,omitempty"` 105 | }{"bob"}, 106 | expect: []byte("my_name = \"bob\"\n"), 107 | }, 108 | // omitempty: 109 | { 110 | v: struct { 111 | Name string `toml:",omitempty"` 112 | }{"bob"}, 113 | expect: []byte("name = \"bob\"\n"), 114 | }, 115 | // slices: 116 | { 117 | v: struct { 118 | Ints []int 119 | }{[]int{1, 2, 3}}, 120 | expect: []byte("ints = [1, 2, 3]\n"), 121 | }, 122 | { 123 | v: struct { 124 | IntsOfInts [][]int 125 | }{[][]int{{}, {1}, {}, {2}, {3, 4}}}, 126 | expect: []byte("ints_of_ints = [[], [1], [], [2], [3, 4]]\n"), 127 | }, 128 | { 129 | v: struct { 130 | IntsOfInts [][]int 131 | }{[][]int{{1, 2}, {3, 4}}}, 132 | expect: []byte("ints_of_ints = [[1, 2], [3, 4]]\n"), 133 | }, 134 | // heterogenous slices: 135 | { 136 | v: struct { 137 | I []interface{} 138 | }{[]interface{}{ 139 | []int{1, 2}, float64(2), int32(-5), "yo", true, 140 | }}, 141 | expect: []byte("i = [[1, 2], 2e+00, -5, \"yo\", true]\n"), 142 | }, 143 | { 144 | v: struct { 145 | MI []interface{} 146 | }{[]interface{}{ 147 | map[string]int{"a": 1, "b": 2}, 148 | "multi", 149 | }}, 150 | expect: []byte("m_i = [{a = 1, b = 2}, \"multi\"]\n"), 151 | }, 152 | { 153 | v: struct { 154 | MI []interface{} 155 | }{[]interface{}{ 156 | "multi", 157 | map[string]int{"a": 1, "b": 2}, 158 | }}, 159 | expect: []byte("m_i = [\"multi\", {a = 1, b = 2}]\n"), 160 | }, 161 | { 162 | v: struct { 163 | MI []interface{} 164 | }{[]interface{}{ 165 | "multimulti", 166 | map[string]interface{}{ 167 | "b": 1, 168 | "m": map[string]interface{}{ 169 | "x": 2, 170 | "y": []int{3, 4}, 171 | }, 172 | }, 173 | }}, 174 | expect: []byte("m_i = [\"multimulti\", {b = 1, m = {x = 2, y = [3, 4]}}]\n"), 175 | }, 176 | 177 | // pointer: 178 | { 179 | v: struct{ Named *Name }{&Name{First: "name"}}, 180 | expect: []byte("[named]\nfirst = \"name\"\nlast = \"\"\n"), 181 | }, 182 | // canon test document: 183 | { 184 | v: theTestStruct(), 185 | expect: loadTestData("marshal-teststruct.toml"), 186 | }, 187 | // funky map key types: 188 | { 189 | v: map[string]interface{}{ 190 | "intKeys": map[int]int{1: 1, 2: 2, 3: 3}, 191 | "marshalerKeys": map[time.Time]int{time.Time{}: 1}, 192 | }, 193 | expect: loadTestData("marshal-funkymapkeys.toml"), 194 | }, 195 | // Marshaler: 196 | { 197 | v: map[string]interface{}{ 198 | "m1": testMarshaler{"1"}, 199 | "m2": &testMarshaler{"2"}, 200 | "m3": &testMarshalerPtr{"3"}, 201 | }, 202 | expect: loadTestData("marshal-marshaler.toml"), 203 | }, 204 | // MarshalerRec: 205 | { 206 | v: map[string]interface{}{ 207 | "m1": testMarshalerRec{1}, 208 | "m2": &testMarshalerRec{2}, 209 | "m3": &testMarshalerRecPtr{3}, 210 | "sub": &testMarshalerRec{map[string]interface{}{ 211 | "key": 1, 212 | }}, 213 | }, 214 | expect: loadTestData("marshal-marshalerrec.toml"), 215 | }, 216 | // key escaping: 217 | { 218 | v: map[string]interface{}{ 219 | "": "empty", 220 | " ": "space", 221 | "ʎǝʞ": "reverse", 222 | "1": "number (not quoted)", 223 | "-": "dash (not quoted)", 224 | "subtable with space": map[string]interface{}{ 225 | "depth": 1, 226 | "subsubtable with space": map[string]interface{}{ 227 | "depth": 2, 228 | }, 229 | }, 230 | }, 231 | expect: loadTestData("marshal-key-escape.toml"), 232 | }, 233 | // empty interface: 234 | { 235 | v: func() interface{} { 236 | var v interface{} = map[string]interface{}{"foo": "bar"} 237 | return &v 238 | }(), 239 | expect: []byte("foo = \"bar\"\n"), 240 | }, 241 | } 242 | 243 | func TestMarshal(t *testing.T) { 244 | for _, test := range marshalTests { 245 | b, err := Marshal(test.v) 246 | if err != nil { 247 | t.Errorf("Unexpected error %v\nfor value:\n%s", err, pretty.Sprint(test.v)) 248 | } 249 | if d := checkOutput(b, test.expect); d != "" { 250 | t.Errorf("Output mismatch:\nValue:\n%s\nDiff:\n%s", pretty.Sprint(test.v), d) 251 | } 252 | } 253 | } 254 | 255 | func TestMarshalRoundTrip(t *testing.T) { 256 | v := theTestStruct() 257 | b, err := Marshal(v) 258 | if err != nil { 259 | t.Error("Unexpected Marshal error:", err) 260 | } 261 | dest := testStruct{} 262 | if err := Unmarshal(b, &dest); err != nil { 263 | t.Error("Unmarshal error:", err) 264 | } 265 | if !reflect.DeepEqual(theTestStruct(), &dest) { 266 | t.Errorf("Unmarshaled value mismatch:\n%s", pretty.Compare(v, dest)) 267 | } 268 | } 269 | 270 | func TestMarshalArrayTableEmptyParent(t *testing.T) { 271 | type Baz struct { 272 | Key int 273 | } 274 | type Bar struct { 275 | Baz Baz 276 | } 277 | type Foo struct { 278 | Bars []Bar 279 | } 280 | 281 | v := Foo{[]Bar{{Baz{1}}, {Baz{2}}}} 282 | b, err := Marshal(v) 283 | if err != nil { 284 | t.Fatal(err) 285 | } 286 | if d := checkOutput(b, loadTestData("marshal-arraytable-empty.toml")); d != "" { 287 | t.Errorf("Output mismatch:\n%s", d) 288 | } 289 | } 290 | 291 | func TestMarshalPointerError(t *testing.T) { 292 | type X struct{ Sub *X } 293 | want := &marshalNilError{reflect.TypeOf((*X)(nil))} 294 | 295 | if _, err := Marshal((*X)(nil)); !reflect.DeepEqual(err, want) { 296 | t.Errorf("Got %q, expected %q", err, want) 297 | } 298 | if _, err := Marshal(&X{nil}); !reflect.DeepEqual(err, want) { 299 | t.Errorf("Got %q, expected %q", err, want) 300 | } 301 | } 302 | 303 | func TestMarshalNonStruct(t *testing.T) { 304 | val := []string{} 305 | want := &marshalTableError{reflect.TypeOf(val)} 306 | if _, err := Marshal(val); !reflect.DeepEqual(err, want) { 307 | t.Errorf("Got %q, expected %q", err, want) 308 | } 309 | } 310 | 311 | func TestMarshalOmitempty(t *testing.T) { 312 | var x struct { 313 | ZeroArray [0]int `toml:",omitempty"` 314 | ZeroArray2 [10]int `toml:",omitempty"` 315 | Slice []int `toml:",omitempty"` 316 | Pointer *int `toml:",omitempty"` 317 | Int int `toml:",omitempty"` 318 | } 319 | out, err := Marshal(x) 320 | if err != nil { 321 | t.Fatal(err) 322 | } 323 | if len(out) > 0 { 324 | t.Fatalf("want no output, got %q", out) 325 | } 326 | } 327 | 328 | func checkOutput(got, want []byte) string { 329 | if bytes.Equal(got, want) { 330 | return "" 331 | } 332 | return diff.Diff(string(got), string(want)) 333 | } 334 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | ) 8 | 9 | // LineError is returned by Unmarshal, UnmarshalTable and Parse 10 | // if the error is local to a line. 11 | type LineError struct { 12 | Line int 13 | StructField string 14 | Err error 15 | } 16 | 17 | func (err *LineError) Error() string { 18 | field := "" 19 | if err.StructField != "" { 20 | field = "(" + err.StructField + ") " 21 | } 22 | return fmt.Sprintf("line %d: %s%v", err.Line, field, err.Err) 23 | } 24 | 25 | func (err *LineError) Unwrap() error { 26 | return err.Err 27 | } 28 | 29 | func lineError(line int, err error) error { 30 | if err == nil { 31 | return nil 32 | } 33 | if _, ok := err.(*LineError); ok { 34 | return err 35 | } 36 | return &LineError{Line: line, Err: err} 37 | } 38 | 39 | func lineErrorField(line int, field string, err error) error { 40 | if lerr, ok := err.(*LineError); ok { 41 | return lerr 42 | } else if err != nil { 43 | err = &LineError{Line: line, StructField: field, Err: err} 44 | } 45 | return err 46 | } 47 | 48 | type rawControlError struct { 49 | char rune 50 | } 51 | 52 | func (err *rawControlError) Error() string { 53 | return fmt.Sprintf("raw control character %q", err.char) 54 | } 55 | 56 | type overflowError struct { 57 | kind reflect.Kind 58 | v string 59 | } 60 | 61 | func (err *overflowError) Error() string { 62 | return fmt.Sprintf("value %s is out of range for %v", err.v, err.kind) 63 | } 64 | 65 | func convertNumError(kind reflect.Kind, err error) error { 66 | if numerr, ok := err.(*strconv.NumError); ok && numerr.Err == strconv.ErrRange { 67 | return &overflowError{kind, numerr.Num} 68 | } 69 | return err 70 | } 71 | 72 | type invalidUnmarshalError struct { 73 | typ reflect.Type 74 | } 75 | 76 | func (err *invalidUnmarshalError) Error() string { 77 | if err.typ == nil { 78 | return "toml: Unmarshal(nil)" 79 | } 80 | if err.typ.Kind() != reflect.Ptr { 81 | return "toml: Unmarshal(non-pointer " + err.typ.String() + ")" 82 | } 83 | return "toml: Unmarshal(nil " + err.typ.String() + ")" 84 | } 85 | 86 | type unmarshalTypeError struct { 87 | what string 88 | want string 89 | typ reflect.Type 90 | } 91 | 92 | func (err *unmarshalTypeError) Error() string { 93 | msg := fmt.Sprintf("cannot unmarshal TOML %s into %s", err.what, err.typ) 94 | if err.want != "" { 95 | msg += " (need " + err.want + ")" 96 | } 97 | return msg 98 | } 99 | 100 | type marshalNilError struct { 101 | typ reflect.Type 102 | } 103 | 104 | func (err *marshalNilError) Error() string { 105 | return fmt.Sprintf("toml: cannot marshal nil %s", err.typ) 106 | } 107 | 108 | type marshalTableError struct { 109 | typ reflect.Type 110 | } 111 | 112 | func (err *marshalTableError) Error() string { 113 | return fmt.Sprintf("toml: cannot marshal %s as table, want struct or map type", err.typ) 114 | } 115 | -------------------------------------------------------------------------------- /example_decoder_test.go: -------------------------------------------------------------------------------- 1 | package toml_test 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "time" 8 | 9 | "github.com/naoina/toml" 10 | ) 11 | 12 | type Config struct { 13 | Title string 14 | Owner struct { 15 | Name string 16 | Org string `toml:"organization"` 17 | Bio string 18 | Dob time.Time 19 | } 20 | Database struct { 21 | Server string 22 | Ports []int 23 | ConnectionMax uint 24 | Enabled bool 25 | } 26 | Servers map[string]ServerInfo 27 | Clients struct { 28 | Data [][]interface{} 29 | Hosts []string 30 | } 31 | } 32 | 33 | type ServerInfo struct { 34 | IP net.IP 35 | DC string 36 | } 37 | 38 | func Example() { 39 | f, err := os.Open("testdata/example.toml") 40 | if err != nil { 41 | panic(err) 42 | } 43 | defer f.Close() 44 | var config Config 45 | if err := toml.NewDecoder(f).Decode(&config); err != nil { 46 | panic(err) 47 | } 48 | 49 | fmt.Println("IP of server 'alpha':", config.Servers["alpha"].IP) 50 | // Output: IP of server 'alpha': 10.0.0.1 51 | } 52 | -------------------------------------------------------------------------------- /example_marshaler_rec_test.go: -------------------------------------------------------------------------------- 1 | package toml_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/naoina/toml" 10 | ) 11 | 12 | // This example shows how the UnmarshalerRec interface can be used to apply field 13 | // validations and default values. 14 | 15 | type Server struct { 16 | Addr string // ":" 17 | Timeout time.Duration // defaults to 10s 18 | } 19 | 20 | // UnmarshalTOML implements toml.Unmarshaler. 21 | func (s *Server) UnmarshalTOML(decode func(interface{}) error) error { 22 | // Parse the input into type tomlServer, which defines the 23 | // expected format of Server in TOML. 24 | type tomlServer struct { 25 | Addr string 26 | Timeout string 27 | } 28 | var dec tomlServer 29 | if err := decode(&dec); err != nil { 30 | return err 31 | } 32 | 33 | // Validate the address. 34 | if dec.Addr == "" { 35 | return errors.New("missing server address") 36 | } 37 | _, _, err := net.SplitHostPort(dec.Addr) 38 | if err != nil { 39 | return fmt.Errorf("invalid server address %q: %v", dec.Addr, err) 40 | } 41 | // Validate the timeout and apply the default value. 42 | var timeout time.Duration 43 | if dec.Timeout == "" { 44 | timeout = 10 * time.Second 45 | } else if timeout, err = time.ParseDuration(dec.Timeout); err != nil { 46 | return fmt.Errorf("invalid server timeout %q: %v", dec.Timeout, err) 47 | } 48 | 49 | // Assign the decoded value. 50 | *s = Server{Addr: dec.Addr, Timeout: timeout} 51 | return nil 52 | } 53 | 54 | func ExampleUnmarshalerRec() { 55 | input := []byte(` 56 | [[servers]] 57 | addr = "198.51.100.3:80" 58 | 59 | [[servers]] 60 | addr = "192.0.2.10:8080" 61 | timeout = "30s" 62 | `) 63 | var config struct { 64 | Servers []Server 65 | } 66 | toml.Unmarshal(input, &config) 67 | fmt.Printf("Unmarshaled:\n%+v\n\n", config) 68 | 69 | // Output: 70 | // Unmarshaled: 71 | // {Servers:[{Addr:198.51.100.3:80 Timeout:10s} {Addr:192.0.2.10:8080 Timeout:30s}]} 72 | } 73 | -------------------------------------------------------------------------------- /example_marshaler_test.go: -------------------------------------------------------------------------------- 1 | package toml_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/naoina/toml" 7 | ) 8 | 9 | // This example shows how the Unmarshaler interface can be used to access the TOML source 10 | // input during decoding. 11 | 12 | // RawTOML stores the raw TOML syntax that was passed to UnmarshalTOML. 13 | type RawTOML []byte 14 | 15 | func (r *RawTOML) UnmarshalTOML(input []byte) error { 16 | cpy := make([]byte, len(input)) 17 | copy(cpy, input) 18 | *r = cpy 19 | return nil 20 | } 21 | 22 | func ExampleUnmarshaler() { 23 | input := []byte(` 24 | foo = 1 25 | 26 | [[servers]] 27 | addr = "198.51.100.3:80" # a comment 28 | 29 | [[servers]] 30 | addr = "192.0.2.10:8080" 31 | timeout = "30s" 32 | `) 33 | var config struct { 34 | Foo int 35 | Servers RawTOML 36 | } 37 | toml.Unmarshal(input, &config) 38 | fmt.Printf("config.Foo = %d\n", config.Foo) 39 | fmt.Printf("config.Servers =\n%s\n", indent(config.Servers, 2)) 40 | 41 | // Output: 42 | // config.Foo = 1 43 | // config.Servers = 44 | // [[servers]] 45 | // addr = "198.51.100.3:80" # a comment 46 | // [[servers]] 47 | // addr = "192.0.2.10:8080" 48 | // timeout = "30s" 49 | } 50 | -------------------------------------------------------------------------------- /example_text_marshaler_test.go: -------------------------------------------------------------------------------- 1 | package toml_test 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/naoina/toml" 8 | ) 9 | 10 | func Example_textUnmarshaler() { 11 | type Config struct { 12 | Servers []net.IP 13 | } 14 | 15 | input := []byte(` 16 | servers = ["192.0.2.10", "198.51.100.3"] 17 | `) 18 | 19 | var config Config 20 | toml.Unmarshal(input, &config) 21 | fmt.Printf("Unmarshaled:\n%+v\n\n", config) 22 | 23 | output, _ := toml.Marshal(&config) 24 | fmt.Printf("Marshaled:\n%s", output) 25 | 26 | // Output: 27 | // Unmarshaled: 28 | // {Servers:[192.0.2.10 198.51.100.3]} 29 | // 30 | // Marshaled: 31 | // servers = ["192.0.2.10", "198.51.100.3"] 32 | } 33 | 34 | func Example_textUnmarshalerError() { 35 | type Config struct { 36 | Servers []net.IP 37 | } 38 | 39 | input := []byte(` 40 | servers = ["192.0.2.10", "198.51.100.500"] 41 | `) 42 | 43 | var config Config 44 | err := toml.Unmarshal(input, &config) 45 | fmt.Printf("Unmarshal error:\n%v", err) 46 | 47 | // Output: 48 | // Unmarshal error: 49 | // line 2: (toml_test.Config.Servers) invalid IP address: 198.51.100.500 50 | } 51 | -------------------------------------------------------------------------------- /example_util_test.go: -------------------------------------------------------------------------------- 1 | package toml_test 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func indent(b []byte, spaces int) string { 8 | space := strings.Repeat(" ", spaces) 9 | return space + strings.Replace(string(b), "\n", "\n"+space, -1) 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/naoina/toml 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/kylelemons/godebug v1.1.0 7 | github.com/naoina/go-stringutil v0.1.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 2 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 3 | github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= 4 | github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= 5 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/naoina/toml/ast" 10 | ) 11 | 12 | // The parser is generated by github.com/pointlander/peg. To regenerate it, do: 13 | // 14 | // go get -u github.com/pointlander/peg 15 | // go generate . 16 | 17 | //go:generate peg -switch -inline parse.peg 18 | 19 | var ( 20 | errParse = errors.New("invalid TOML syntax") 21 | errNewlineRequired = errors.New("newline required in table") 22 | errInlineTableCommaRequired = errors.New("missing ',' in inline table") 23 | errInlineTableCommaAtEnd = errors.New("inline table cannot contain ',' after last key/value pair") 24 | ) 25 | 26 | var ( 27 | underscoreReplacer = strings.NewReplacer("_", "") 28 | timeLetterReplacer = strings.NewReplacer("z", "Z", "t", "T") 29 | escapeReplacer = strings.NewReplacer( 30 | "\b", "\\n", 31 | "\f", "\\f", 32 | "\n", "\\n", 33 | "\r", "\\r", 34 | "\t", "\\t", 35 | ) 36 | ) 37 | 38 | // Parse returns an AST representation of TOML. 39 | // The toplevel is represented by a table. 40 | func Parse(data []byte) (*ast.Table, error) { 41 | d := &parseState{p: &tomlParser{Buffer: string(data)}} 42 | d.init() 43 | 44 | if err := d.parse(); err != nil { 45 | return nil, err 46 | } 47 | 48 | return d.p.toml.topTable, nil 49 | } 50 | 51 | type parseState struct { 52 | p *tomlParser 53 | } 54 | 55 | func (d *parseState) init() { 56 | d.p.Init() 57 | d.p.toml.init(d.p.buffer) 58 | } 59 | 60 | func (d *parseState) parse() error { 61 | if err := d.p.Parse(); err != nil { 62 | if err, ok := err.(*parseError); ok { 63 | return lineError(err.Line(), errParse) 64 | // return lineError(err.Line(), errors.New("parse error:\n"+d.p.SprintSyntaxTree())) 65 | } 66 | return err 67 | } 68 | return d.execute() 69 | } 70 | 71 | func (d *parseState) execute() (err error) { 72 | defer func() { 73 | if e := recover(); e != nil { 74 | lerr, ok := e.(*LineError) 75 | if !ok { 76 | panic(e) 77 | } 78 | err = lerr 79 | } 80 | }() 81 | d.p.Execute() 82 | return nil 83 | } 84 | 85 | func (e *parseError) Line() int { 86 | tokens := []token32{e.max} 87 | positions, p := make([]int, 2*len(tokens)), 0 88 | for _, token := range tokens { 89 | positions[p], p = int(token.begin), p+1 90 | positions[p], p = int(token.end), p+1 91 | } 92 | for _, t := range translatePositions(e.p.buffer, positions) { 93 | if e.p.line < t.line { 94 | e.p.line = t.line 95 | } 96 | } 97 | return e.p.line 98 | } 99 | 100 | type tabStackElem struct { 101 | key string 102 | table *ast.Table 103 | } 104 | 105 | type array struct { 106 | parent *array 107 | child *array 108 | a ast.Array 109 | line int 110 | } 111 | 112 | type toml struct { 113 | topTable *ast.Table // the top-level table 114 | line int // the current line number 115 | curTable *ast.Table // the current table 116 | curArray *array // the current array 117 | stringBuf string // temporary buffer for string values 118 | key string // the current table key 119 | tableKeyAcc []string // accumulator for dotted keys 120 | val ast.Value // last decoded value 121 | tabStack []*tabStackElem // table stack (for inline tables) 122 | } 123 | 124 | func (p *toml) init(data []rune) { 125 | p.line = 1 126 | p.topTable = p.newTable(ast.TableTypeNormal, "") 127 | p.topTable.Position.End = len(data) - 1 128 | p.topTable.Data = data[:len(data)-1] // truncate the end_symbol added by PEG parse generator. 129 | p.curTable = p.topTable 130 | } 131 | 132 | func (p *toml) Error(err error) { 133 | panic(lineError(p.line, err)) 134 | } 135 | 136 | // Newline is called whenever the parser moves to a new line. 137 | func (p *toml) Newline() { 138 | p.line++ 139 | } 140 | 141 | // -- Primitive Value Callbacks -- 142 | 143 | func (p *tomlParser) SetTime(begin, end int) { 144 | // Make value compatible with time.Parse. 145 | v := timeLetterReplacer.Replace(string(p.buffer[begin:end])) 146 | p.val = &ast.Datetime{ 147 | Position: ast.Position{Begin: begin, End: end}, 148 | Data: p.buffer[begin:end], 149 | Value: v, 150 | } 151 | } 152 | 153 | func (p *tomlParser) SetFloat(begin, end int) { 154 | // Make value compatible with strconv.ParseFloat. 155 | v := underscoreReplacer.Replace(string(p.buffer[begin:end])) 156 | if v == "+nan" || v == "-nan" { 157 | v = "nan" 158 | } 159 | p.val = &ast.Float{ 160 | Position: ast.Position{Begin: begin, End: end}, 161 | Data: p.buffer[begin:end], 162 | Value: v, 163 | } 164 | } 165 | 166 | func (p *tomlParser) SetInteger(begin, end int) { 167 | p.val = &ast.Integer{ 168 | Position: ast.Position{Begin: begin, End: end}, 169 | Data: p.buffer[begin:end], 170 | Value: underscoreReplacer.Replace(string(p.buffer[begin:end])), 171 | } 172 | } 173 | 174 | func (p *tomlParser) SetString(begin, end int) { 175 | p.val = &ast.String{ 176 | Position: ast.Position{Begin: begin, End: end}, 177 | Data: p.buffer[begin:end], 178 | Value: p.stringBuf, 179 | } 180 | p.stringBuf = "" 181 | } 182 | 183 | func (p *tomlParser) SetBool(begin, end int) { 184 | p.val = &ast.Boolean{ 185 | Position: ast.Position{Begin: begin, End: end}, 186 | Data: p.buffer[begin:end], 187 | Value: string(p.buffer[begin:end]), 188 | } 189 | } 190 | 191 | // -- String Callbacks -- 192 | // 193 | // These run during string parsing and build up the string in p.stringBuf. 194 | 195 | func (p *toml) SetBasicString(buf []rune, begin, end int) { 196 | p.stringBuf = p.unquote(string(buf[begin:end])) 197 | } 198 | 199 | func (p *toml) SetMultilineBasicString() { 200 | p.stringBuf = p.unquote(`"` + escapeReplacer.Replace(strings.TrimLeft(p.stringBuf, "\r\n")) + `"`) 201 | } 202 | 203 | func (p *toml) AddMultilineBasicBody(buf []rune, begin, end int) { 204 | p.stringBuf += string(buf[begin:end]) 205 | } 206 | 207 | func (p *toml) AddMultilineBasicQuote() { 208 | p.stringBuf += "\\\"" 209 | } 210 | 211 | func (p *toml) SetLiteralString(buf []rune, begin, end int) { 212 | p.stringBuf = string(buf[begin:end]) 213 | } 214 | 215 | func (p *toml) SetMultilineLiteralString(buf []rune, begin, end int) { 216 | p.stringBuf = strings.TrimLeft(string(buf[begin:end]), "\r\n") 217 | } 218 | 219 | func (p *toml) unquote(s string) string { 220 | s, err := strconv.Unquote(s) 221 | if err != nil { 222 | p.Error(err) 223 | } 224 | return s 225 | } 226 | 227 | // -- Array Callbacks -- 228 | // 229 | // These callbacks maintain the array stack and accumulate elements. 230 | 231 | func (p *toml) StartArray() { 232 | if p.curArray == nil { 233 | p.curArray = &array{line: p.line} 234 | return 235 | } 236 | p.curArray.child = &array{parent: p.curArray, line: p.line} 237 | p.curArray = p.curArray.child 238 | } 239 | 240 | func (p *toml) AddArrayVal() { 241 | p.curArray.a.Value = append(p.curArray.a.Value, p.val) 242 | } 243 | 244 | func (p *tomlParser) SetArray(begin, end int) { 245 | p.curArray.a.Position = ast.Position{Begin: begin, End: end} 246 | p.curArray.a.Data = p.buffer[begin:end] 247 | p.val = &p.curArray.a 248 | p.curArray = p.curArray.parent 249 | } 250 | 251 | // -- Table Callbacks -- 252 | 253 | func (p *toml) SetTable(buf []rune, begin, end int) { 254 | rawName := string(buf[begin:end]) 255 | p.setTable(p.topTable, rawName, p.tableKeyAcc) 256 | p.tableKeyAcc = nil 257 | } 258 | 259 | func (p *toml) setTable(parent *ast.Table, name string, names []string) { 260 | parent, err := p.lookupTable(parent, names[:len(names)-1]) 261 | if err != nil { 262 | p.Error(err) 263 | } 264 | last := names[len(names)-1] 265 | tbl := p.newTable(ast.TableTypeNormal, last) 266 | switch v := parent.Fields[last].(type) { 267 | case nil: 268 | parent.Fields[last] = tbl 269 | case []*ast.Table: 270 | p.Error(fmt.Errorf("table `%s' is in conflict with array table in line %d", name, v[0].Line)) 271 | case *ast.Table: 272 | if (v.Position == ast.Position{}) { 273 | // This table was created as an implicit parent. 274 | // Replace it with the real defined table. 275 | tbl.Fields = v.Fields 276 | parent.Fields[last] = tbl 277 | } else { 278 | p.Error(fmt.Errorf("table `%s' is in conflict with table in line %d", name, v.Line)) 279 | } 280 | case *ast.KeyValue: 281 | p.Error(fmt.Errorf("table `%s' is in conflict with line %d", name, v.Line)) 282 | default: 283 | p.Error(fmt.Errorf("BUG: table `%s' is in conflict but it's unknown type `%T'", last, v)) 284 | } 285 | p.curTable = tbl 286 | } 287 | 288 | func (p *toml) newTable(typ ast.TableType, name string) *ast.Table { 289 | return &ast.Table{ 290 | Line: p.line, 291 | Name: name, 292 | Type: typ, 293 | Fields: make(map[string]interface{}), 294 | } 295 | } 296 | 297 | func (p *toml) lookupTable(t *ast.Table, keys []string) (*ast.Table, error) { 298 | for _, s := range keys { 299 | val, exists := t.Fields[s] 300 | if !exists { 301 | tbl := p.newTable(ast.TableTypeNormal, s) 302 | t.Fields[s] = tbl 303 | t = tbl 304 | continue 305 | } 306 | switch v := val.(type) { 307 | case *ast.Table: 308 | t = v 309 | case []*ast.Table: 310 | t = v[len(v)-1] 311 | case *ast.KeyValue: 312 | return nil, fmt.Errorf("key `%s' is in conflict with line %d", s, v.Line) 313 | default: 314 | return nil, fmt.Errorf("BUG: key `%s' is in conflict but it's unknown type `%T'", s, v) 315 | } 316 | } 317 | return t, nil 318 | } 319 | 320 | // SetTableSource assigns the source data of a complete table. 321 | func (p *tomlParser) SetTableSource(begin, end int) { 322 | p.curTable.Data = p.buffer[begin:end] 323 | p.curTable.Position.Begin = begin 324 | p.curTable.Position.End = end 325 | } 326 | 327 | func (p *toml) AddTableKey() { 328 | p.tableKeyAcc = append(p.tableKeyAcc, p.key) 329 | } 330 | 331 | // SetKey is called after a table key has been parsed. 332 | func (p *toml) SetKey(buf []rune, begin, end int) { 333 | p.key = string(buf[begin:end]) 334 | if len(p.key) > 0 && p.key[0] == '"' { 335 | p.key = p.unquote(p.key) 336 | } 337 | } 338 | 339 | // AddKeyValue is called after a complete key/value pair has been parsed. 340 | func (p *toml) AddKeyValue() { 341 | if val, exists := p.curTable.Fields[p.key]; exists { 342 | switch v := val.(type) { 343 | case []*ast.Table: 344 | p.Error(fmt.Errorf("key `%s' is in conflict with array table in line %d", p.key, v[0].Line)) 345 | case *ast.Table: 346 | p.Error(fmt.Errorf("key `%s' is in conflict with table in line %d", p.key, v.Line)) 347 | case *ast.KeyValue: 348 | p.Error(fmt.Errorf("key `%s' is in conflict with line %d", p.key, v.Line)) 349 | default: 350 | p.Error(fmt.Errorf("BUG: key `%s' is in conflict but it's unknown type `%T'", p.key, v)) 351 | } 352 | } 353 | p.curTable.Fields[p.key] = &ast.KeyValue{Key: p.key, Value: p.val, Line: p.line} 354 | } 355 | 356 | // -- Array Table Callbacks -- 357 | 358 | func (p *toml) SetArrayTable(buf []rune, begin, end int) { 359 | rawName := string(buf[begin:end]) 360 | p.setArrayTable(p.topTable, rawName, p.tableKeyAcc) 361 | p.tableKeyAcc = nil 362 | } 363 | 364 | func (p *toml) setArrayTable(parent *ast.Table, name string, names []string) { 365 | parent, err := p.lookupTable(parent, names[:len(names)-1]) 366 | if err != nil { 367 | p.Error(err) 368 | } 369 | last := names[len(names)-1] 370 | tbl := p.newTable(ast.TableTypeArray, last) 371 | switch v := parent.Fields[last].(type) { 372 | case nil: 373 | parent.Fields[last] = []*ast.Table{tbl} 374 | case []*ast.Table: 375 | parent.Fields[last] = append(v, tbl) 376 | case *ast.Table: 377 | p.Error(fmt.Errorf("array table `%s' is in conflict with table in line %d", name, v.Line)) 378 | case *ast.KeyValue: 379 | p.Error(fmt.Errorf("array table `%s' is in conflict with line %d", name, v.Line)) 380 | default: 381 | p.Error(fmt.Errorf("BUG: array table `%s' is in conflict but it's unknown type `%T'", name, v)) 382 | } 383 | p.curTable = tbl 384 | } 385 | 386 | // -- Inline Table Callbacks -- 387 | 388 | func (p *toml) StartInlineTable() { 389 | tbl := p.newTable(ast.TableTypeInline, "") 390 | p.tabStack = append(p.tabStack, &tabStackElem{p.key, p.curTable}) 391 | p.curTable = tbl 392 | } 393 | 394 | func (p *toml) EndInlineTable() { 395 | p.val = p.curTable 396 | 397 | // Restore parent table from stack. 398 | st := p.tabStack[len(p.tabStack)-1] 399 | p.key, p.curTable = st.key, st.table 400 | p.tabStack = p.tabStack[:len(p.tabStack)-1] 401 | } 402 | 403 | // SetInlineTableSource sets the source data of an inline table. 404 | // This is called just after parsing the table value, when the table 405 | // is still in p.val. 406 | func (p *tomlParser) SetInlineTableSource(begin, end int) { 407 | tbl := p.val.(*ast.Table) 408 | tbl.Data = p.buffer[begin:end] 409 | tbl.Position.Begin = begin 410 | tbl.Position.End = end 411 | } 412 | -------------------------------------------------------------------------------- /parse.peg: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | type tomlParser Peg { 4 | toml 5 | } 6 | 7 | TOML <- Expression (newline Expression)* newline* !. { _ = buffer } 8 | 9 | Expression <- ( 10 | { p.SetTableSource(begin, end) } 11 | / ws keyval ws comment? 12 | / ws comment? 13 | / ws 14 | ) 15 | 16 | val <- ( 17 | { p.SetTime(begin, end) } 18 | / { p.SetFloat(begin, end) } 19 | / { p.SetInteger(begin, end) } 20 | / { p.SetString(begin, end) } 21 | / { p.SetBool(begin, end) } 22 | / { p.SetArray(begin, end) } 23 | / { p.SetInlineTableSource(begin, end) } 24 | ) 25 | 26 | ws <- [ \t]* 27 | wsnl <- ([ \t] / newline)* 28 | 29 | comment <- '#' (badControl / [\t -\0x10FFFF])* 30 | 31 | newline <- ('\r\n' / '\n') { p.Newline() } 32 | 33 | newlineRequired <- ( 34 | !newline { p.Error(errNewlineRequired) } 35 | / newline 36 | ) 37 | 38 | badControl <- <[\0x7F\0x0B\0xC\0x00-\0x08\0x0E-\0x1F]> { 39 | p.Error(&rawControlError{p.buffer[begin]}) 40 | } 41 | 42 | # ------------------------------------------------------------------------- 43 | # -- Tables 44 | 45 | table <- stdTable / arrayTable 46 | 47 | stdTable <- '[' ws ws ']' { p.SetTable(p.buffer, begin, end) } 48 | 49 | arrayTable <- '[[' ws ws ']]' { p.SetArrayTable(p.buffer, begin, end) } 50 | 51 | keyval <- key ws '=' ws val { p.AddKeyValue() } 52 | 53 | key <- bareKey / quotedKey 54 | 55 | bareKey <- { p.SetKey(p.buffer, begin, end) } 56 | 57 | bareKeyChar <- badControl / [0-9A-Za-z\-_] 58 | 59 | quotedKey <- < '"' basicChar* '"' > { p.SetKey(p.buffer, begin, end) } 60 | 61 | tableKey <- tableKeyComp (tableKeySep tableKeyComp)* 62 | 63 | tableKeyComp <- key { p.AddTableKey() } 64 | 65 | tableKeySep <- ws '.' ws 66 | 67 | # ------------------------------------------------------------------------- 68 | # -- Inline Tables 69 | 70 | inlineTable <- ( 71 | '{' { p.StartInlineTable() } 72 | ws inlineTableKeyValues? ws 73 | '}' { p.EndInlineTable() } 74 | ) 75 | 76 | inlineTableKeyValues <- ( 77 | keyval 78 | ( 79 | ws inlineTableCommaRequired ws 80 | keyval 81 | )* 82 | ws inlineTableCommaForbidden 83 | ) 84 | 85 | inlineTableCommaForbidden <- ( 86 | !',' 87 | / ',' { p.Error(errInlineTableCommaAtEnd) } 88 | ) 89 | 90 | inlineTableCommaRequired <- ( 91 | !',' { p.Error(errInlineTableCommaRequired) } 92 | / ',' 93 | ) 94 | 95 | # ------------------------------------------------------------------------- 96 | # -- Booleans 97 | 98 | boolean <- 'true' / 'false' 99 | 100 | # ------------------------------------------------------------------------- 101 | # -- Numbers 102 | 103 | integer <- hexInt / octalInt / binaryInt / decimalInt / ([+\-] decimalInt) 104 | 105 | decimalInt <- [1-9] (decimalDigit / '_' decimalDigit)+ / decimalDigit 106 | decimalDigit <- [0-9] 107 | 108 | hexInt <- '0x' hexDigit (hexDigit / '_' hexDigit)* 109 | hexDigit <- [[0-9A-F]] 110 | 111 | octalInt <- '0o' octalDigit (octalDigit / '_' octalDigit)* 112 | octalDigit <- [0-7] 113 | 114 | binaryInt <- '0b' binaryDigit (binaryDigit / '_' octalDigit)* 115 | binaryDigit <- [01] 116 | 117 | float <- [+\-]? ('nan' / 'inf' / floatDigits) 118 | floatDigits <- decimalInt (floatFrac floatExp? / floatFrac? floatExp) 119 | floatFrac <- '.' decimalDigit (decimalDigit / '_' decimalDigit)* 120 | floatExp <- [[E]] [\-+]? decimalDigit (decimalDigit / '_' decimalDigit)* 121 | 122 | # ------------------------------------------------------------------------- 123 | # -- Escape Sequences 124 | 125 | escaped <- escape ([btnfr"/\\] / 'u' hexQuad / 'U' hexQuad hexQuad) 126 | escape <- '\\' 127 | 128 | hexQuad <- hexDigit hexDigit hexDigit hexDigit 129 | 130 | # ------------------------------------------------------------------------- 131 | # -- Strings 132 | 133 | string <- ( 134 | mlLiteralString 135 | / literalString 136 | / mlBasicString 137 | / basicString 138 | ) 139 | 140 | basicString <- <'"' basicChar* '"'> { p.SetBasicString(p.buffer, begin, end) } 141 | 142 | basicChar <- badControl / basicUnescaped / escaped 143 | 144 | # This is basically the full printable range, excluding " and \ 145 | basicUnescaped <- [\t -!#-\[\]-\0x10FFFF] 146 | 147 | mlBasicString <- '"""' mlBasicBody '"""' { p.SetMultilineBasicString() } 148 | 149 | mlBasicBody <- ( 150 | mlBasicBodyChar* 151 | mlBasicBodyEndQuotes? # needed for strings like """str""""" 152 | ) 153 | 154 | mlBasicBodyChar <- ( 155 | !'"""' '"' { p.AddMultilineBasicQuote() } 156 | / { p.AddMultilineBasicBody(p.buffer, begin, end) } 157 | / escape newline wsnl 158 | ) 159 | mlBasicBodyEndQuotes <- ( 160 | ('""' &'"""') { p.AddMultilineBasicQuote(); p.AddMultilineBasicQuote() } 161 | / ('"' &'"""') { p.AddMultilineBasicQuote() } 162 | ) 163 | 164 | literalString <- "'" "'" { p.SetLiteralString(p.buffer, begin, end) } 165 | 166 | literalChar <- badControl / [\t -&(-\0x10FFFF] 167 | 168 | mlLiteralString <- ( 169 | "'''" 170 | { p.SetMultilineLiteralString(p.buffer, begin, end) } 171 | "'''" 172 | ) 173 | 174 | mlLiteralBody <- ( 175 | (!"'''" (mlLiteralChar / newline))* 176 | mlLiteralBodyEndQuotes? # needed for '''str''''' 177 | ) 178 | 179 | mlLiteralChar <- badControl / [\t -\0x10FFFF] 180 | mlLiteralBodyEndQuotes <- ("''" &"'''") / ("'" &"'''") 181 | 182 | # ------------------------------------------------------------------------- 183 | # -- Datetimes 184 | 185 | datetime <- (fullDate ([[T ]] fullTime)?) / partialTime 186 | 187 | partialTime <- timeHour ':' timeMinute ':' timeSecond timeSecfrac? 188 | 189 | fullDate <- dateFullYear '-' dateMonth '-' dateMDay 190 | fullTime <- partialTime timeOffset? 191 | 192 | dateFullYear <- digitQuad 193 | dateMonth <- digitDual 194 | dateMDay <- digitDual 195 | timeHour <- digitDual 196 | timeMinute <- digitDual 197 | timeSecond <- digitDual 198 | timeSecfrac <- '.' decimalDigit+ 199 | timeNumoffset <- [\-+] timeHour ':' timeMinute 200 | timeOffset <- [[Z]] / timeNumoffset 201 | 202 | digitDual <- decimalDigit decimalDigit 203 | digitQuad <- digitDual digitDual 204 | 205 | # ------------------------------------------------------------------------- 206 | # -- Arrays 207 | 208 | array <- ( 209 | '[' { p.StartArray() } 210 | wsnl arrayValues? wsnl 211 | ']' 212 | ) 213 | 214 | arrayValues <- ( 215 | (wsnl comment)* wsnl 216 | val { p.AddArrayVal() } 217 | ( 218 | (wsnl comment)* wsnl 219 | arraySep 220 | (wsnl comment)* wsnl 221 | val { p.AddArrayVal() } 222 | )* 223 | (wsnl comment)* wsnl 224 | arraySep? 225 | (wsnl comment)* 226 | ) 227 | 228 | arraySep <- ',' 229 | -------------------------------------------------------------------------------- /testdata/example.toml: -------------------------------------------------------------------------------- 1 | title = "TOML Example" 2 | 3 | [owner] 4 | name = "Tom Preston-Werner" 5 | organization = "GitHub" 6 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 7 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 8 | 9 | [database] 10 | server = "192.168.1.1" 11 | ports = [ 8001, 8001, 8002 ] 12 | connection_max = 5000 13 | enabled = true 14 | 15 | [servers] 16 | 17 | # You can indent as you please. Tabs or spaces. TOML don't care. 18 | [servers.alpha] 19 | ip = "10.0.0.1" 20 | dc = "eqdc10" 21 | 22 | [servers.beta] 23 | ip = "10.0.0.2" 24 | dc = "eqdc10" 25 | 26 | [clients] 27 | data = [ ["gamma", "delta"], [1, 2] ] 28 | 29 | # Line breaks are OK when inside arrays 30 | hosts = [ 31 | "alpha", 32 | "omega" 33 | ] 34 | -------------------------------------------------------------------------------- /testdata/marshal-arraytable-empty.toml: -------------------------------------------------------------------------------- 1 | [[bars]] 2 | [bars.baz] 3 | key = 1 4 | 5 | [[bars]] 6 | [bars.baz] 7 | key = 2 8 | -------------------------------------------------------------------------------- /testdata/marshal-funkymapkeys.toml: -------------------------------------------------------------------------------- 1 | [intKeys] 2 | 1 = 1 3 | 2 = 2 4 | 3 = 3 5 | 6 | [marshalerKeys] 7 | "0001-01-01T00:00:00Z" = 1 8 | -------------------------------------------------------------------------------- /testdata/marshal-key-escape.toml: -------------------------------------------------------------------------------- 1 | "" = "empty" 2 | " " = "space" 3 | - = "dash (not quoted)" 4 | 1 = "number (not quoted)" 5 | "ʎǝʞ" = "reverse" 6 | 7 | ["subtable with space"] 8 | depth = 1 9 | 10 | ["subtable with space"."subsubtable with space"] 11 | depth = 2 12 | -------------------------------------------------------------------------------- /testdata/marshal-marshaler.toml: -------------------------------------------------------------------------------- 1 | m1 = 1 2 | m2 = 2 3 | m3 = 3 4 | -------------------------------------------------------------------------------- /testdata/marshal-marshalerrec.toml: -------------------------------------------------------------------------------- 1 | m1 = 1 2 | m2 = 2 3 | m3 = 3 4 | 5 | [sub] 6 | key = 1 7 | -------------------------------------------------------------------------------- /testdata/marshal-teststruct.toml: -------------------------------------------------------------------------------- 1 | [table] 2 | key = "value" 3 | 4 | [table.subtable] 5 | key = "another value" 6 | 7 | [table.inline.name] 8 | first = "Tom" 9 | last = "Preston-Werner" 10 | 11 | [table.inline.point] 12 | x = 1 13 | y = 2 14 | 15 | [string.basic] 16 | basic = "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF." 17 | 18 | [string.multiline] 19 | key1 = "One\nTwo" 20 | key2 = "One\nTwo" 21 | key3 = "One\nTwo" 22 | 23 | [string.multiline.continued] 24 | key1 = "The quick brown fox jumps over the lazy dog." 25 | key2 = "The quick brown fox jumps over the lazy dog." 26 | key3 = "The quick brown fox jumps over the lazy dog." 27 | 28 | [string.literal] 29 | winpath = "C:\\Users\\nodejs\\templates" 30 | winpath2 = "\\\\ServerX\\admin$\\system32\\" 31 | quoted = "Tom \"Dubs\" Preston-Werner" 32 | regex = "<\\i\\c*\\s*>" 33 | 34 | [string.literal.multiline] 35 | regex2 = "I [dw]on't need \\d{2} apples" 36 | lines = "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n" 37 | 38 | [integer] 39 | key1 = 99 40 | key2 = 42 41 | key3 = 0 42 | key4 = -17 43 | 44 | [integer.underscores] 45 | key1 = 1000 46 | key2 = 5349221 47 | key3 = 12345 48 | 49 | [float.fractional] 50 | key1 = 1e+00 51 | key2 = 3.1415e+00 52 | key3 = -1e-02 53 | 54 | [float.exponent] 55 | key1 = 5e+22 56 | key2 = 1e+06 57 | key3 = -2e-02 58 | 59 | [float.both] 60 | key = 6.626e-34 61 | 62 | [float.underscores] 63 | key1 = 9.224617445991227e+06 64 | key2 = 1e+100 65 | 66 | [boolean] 67 | true = true 68 | false = false 69 | 70 | [datetime] 71 | key1 = 1979-05-27T07:32:00Z 72 | key2 = 1979-05-27T00:32:00-07:00 73 | key3 = 1979-05-27T00:32:00.999999-07:00 74 | 75 | [array] 76 | key1 = [1, 2, 3] 77 | key2 = ["red", "yellow", "green"] 78 | key3 = [[1, 2], [3, 4, 5]] 79 | key4 = [[1, 2], ["a", "b", "c"]] 80 | key5 = [1, 2, 3] 81 | key6 = [1, 2] 82 | 83 | [[products]] 84 | name = "Hammer" 85 | sku = 738594937 86 | 87 | [[products]] 88 | 89 | [[products]] 90 | name = "Nail" 91 | sku = 284758393 92 | color = "gray" 93 | 94 | [[fruit]] 95 | name = "apple" 96 | 97 | [fruit.physical] 98 | color = "red" 99 | shape = "round" 100 | 101 | [[fruit.variety]] 102 | name = "red delicious" 103 | 104 | [[fruit.variety]] 105 | name = "granny smith" 106 | 107 | [[fruit]] 108 | name = "banana" 109 | 110 | [fruit.physical] 111 | color = "" 112 | shape = "" 113 | 114 | [[fruit.variety]] 115 | name = "plantain" 116 | -------------------------------------------------------------------------------- /testdata/test.toml: -------------------------------------------------------------------------------- 1 | # test.toml 2 | 3 | ################################################################################ 4 | ## Comment 5 | 6 | # Speak your mind with the hash symbol. They go from the symbol to the end of 7 | # the line. 8 | 9 | 10 | ################################################################################ 11 | ## Table 12 | 13 | # Tables (also known as hash tables or dictionaries) are collections of 14 | # key/value pairs. They appear in square brackets on a line by themselves. 15 | 16 | [table] 17 | 18 | key = "value" # Yeah, you can do this. 19 | 20 | # Nested tables are denoted by table names with dots in them. Name your tables 21 | # whatever crap you please, just don't use #, ., [ or ]. 22 | 23 | [table.subtable] 24 | 25 | key = "another value" 26 | 27 | # You don't need to specify all the super-tables if you don't want to. TOML 28 | # knows how to do it for you. 29 | 30 | # [x] you 31 | # [x.y] don't 32 | # [x.y.z] need these 33 | [x.y.z.w] # for this to work 34 | 35 | 36 | ################################################################################ 37 | ## Inline Table 38 | 39 | # Inline tables provide a more compact syntax for expressing tables. They are 40 | # especially useful for grouped data that can otherwise quickly become verbose. 41 | # Inline tables are enclosed in curly braces `{` and `}`. No newlines are 42 | # allowed between the curly braces unless they are valid within a value. 43 | 44 | [table.inline] 45 | 46 | name = { first = "Tom", last = "Preston-Werner" } 47 | point = { x = 1, y = 2 } 48 | 49 | 50 | ################################################################################ 51 | ## String 52 | 53 | # There are four ways to express strings: basic, multi-line basic, literal, and 54 | # multi-line literal. All strings must contain only valid UTF-8 characters. 55 | 56 | [string.basic] 57 | 58 | basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF." 59 | 60 | [string.multiline] 61 | 62 | # The following strings are byte-for-byte equivalent: 63 | key1 = "One\nTwo" 64 | key2 = """One\nTwo""" 65 | key3 = """ 66 | One 67 | Two""" 68 | 69 | [string.multiline.continued] 70 | 71 | # The following strings are byte-for-byte equivalent: 72 | key1 = "The quick brown fox jumps over the lazy dog." 73 | 74 | key2 = """ 75 | The quick brown \ 76 | 77 | 78 | fox jumps over \ 79 | the lazy dog.""" 80 | 81 | key3 = """\ 82 | The quick brown \ 83 | fox jumps over \ 84 | the lazy dog.\ 85 | """ 86 | 87 | [string.literal] 88 | 89 | # What you see is what you get. 90 | winpath = 'C:\Users\nodejs\templates' 91 | winpath2 = '\\ServerX\admin$\system32\' 92 | quoted = 'Tom "Dubs" Preston-Werner' 93 | regex = '<\i\c*\s*>' 94 | 95 | 96 | [string.literal.multiline] 97 | 98 | regex2 = '''I [dw]on't need \d{2} apples''' 99 | lines = ''' 100 | The first newline is 101 | trimmed in raw strings. 102 | All other whitespace 103 | is preserved. 104 | ''' 105 | 106 | 107 | ################################################################################ 108 | ## Integer 109 | 110 | # Integers are whole numbers. Positive numbers may be prefixed with a plus sign. 111 | # Negative numbers are prefixed with a minus sign. 112 | 113 | [integer] 114 | 115 | key1 = +99 116 | key2 = 42 117 | key3 = 0 118 | key4 = -17 119 | 120 | [integer.underscores] 121 | 122 | # For large numbers, you may use underscores to enhance readability. Each 123 | # underscore must be surrounded by at least one digit. 124 | key1 = 1_000 125 | key2 = 5_349_221 126 | key3 = 1_2_3_4_5 # valid but inadvisable 127 | 128 | 129 | ################################################################################ 130 | ## Float 131 | 132 | # A float consists of an integer part (which may be prefixed with a plus or 133 | # minus sign) followed by a fractional part and/or an exponent part. 134 | 135 | [float.fractional] 136 | 137 | key1 = +1.0 138 | key2 = 3.1415 139 | key3 = -0.01 140 | 141 | [float.exponent] 142 | 143 | key1 = 5e+22 144 | key2 = 1e6 145 | key3 = -2E-2 146 | 147 | [float.both] 148 | 149 | key = 6.626e-34 150 | 151 | [float.underscores] 152 | 153 | key1 = 9_224_617.445_991_228_313 154 | key2 = 1e1_00 155 | 156 | 157 | ################################################################################ 158 | ## Boolean 159 | 160 | # Booleans are just the tokens you're used to. Always lowercase. 161 | 162 | [boolean] 163 | 164 | True = true 165 | False = false 166 | 167 | 168 | ################################################################################ 169 | ## Datetime 170 | 171 | # Datetimes are RFC 3339 dates. 172 | 173 | [datetime] 174 | 175 | key1 = 1979-05-27T07:32:00Z 176 | key2 = 1979-05-27T00:32:00-07:00 177 | key3 = 1979-05-27T00:32:00.999999-07:00 178 | 179 | 180 | ################################################################################ 181 | ## Array 182 | 183 | # Arrays are square brackets with other primitives inside. Whitespace is 184 | # ignored. Elements are separated by commas. Data types may not be mixed. 185 | 186 | [array] 187 | 188 | key1 = [ 1, 2, 3 ] 189 | key2 = [ "red", "yellow", "green" ] 190 | key3 = [ [ 1, 2 ], [3, 4, 5] ] 191 | key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok 192 | 193 | # Arrays can also be multiline. So in addition to ignoring whitespace, arrays 194 | # also ignore newlines between the brackets. Terminating commas are ok before 195 | # the closing bracket. 196 | 197 | key5 = [ 198 | 1, 2, 3 199 | ] 200 | key6 = [ 201 | 1, 202 | 2, # this is ok 203 | ] 204 | 205 | 206 | ################################################################################ 207 | ## Array of Tables 208 | 209 | # These can be expressed by using a table name in double brackets. Each table 210 | # with the same double bracketed name will be an element in the array. The 211 | # tables are inserted in the order encountered. 212 | 213 | [[products]] 214 | 215 | name = "Hammer" 216 | sku = 738594937 217 | 218 | [[products]] 219 | 220 | [[products]] 221 | 222 | name = "Nail" 223 | sku = 284758393 224 | color = "gray" 225 | 226 | 227 | # You can create nested arrays of tables as well. 228 | 229 | [[fruit]] 230 | name = "apple" 231 | 232 | [fruit.physical] 233 | color = "red" 234 | shape = "round" 235 | 236 | [[fruit.variety]] 237 | name = "red delicious" 238 | 239 | [[fruit.variety]] 240 | name = "granny smith" 241 | 242 | [[fruit]] 243 | name = "banana" 244 | 245 | [[fruit.variety]] 246 | name = "plantain" 247 | -------------------------------------------------------------------------------- /testdata/unmarshal-array-1.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-array-1.toml 2 | 3 | ints = [ 4 | 1, 2, 3 5 | ] 6 | -------------------------------------------------------------------------------- /testdata/unmarshal-array-2.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-array-2.toml 2 | 3 | ints = [ 4 | 1 5 | , 2 6 | , 3 7 | ] 8 | -------------------------------------------------------------------------------- /testdata/unmarshal-array-3.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-array-3.toml 2 | 3 | ints = [ 4 | 1, 5 | 2, 6 | 3, # this is ok 7 | ] 8 | -------------------------------------------------------------------------------- /testdata/unmarshal-array-4.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-array-4.toml 2 | 3 | ints = [ 4 | 1, 5 | 2, # this is ok 6 | 3 7 | ] 8 | -------------------------------------------------------------------------------- /testdata/unmarshal-array-5.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-array-5.toml 2 | 3 | ints = [ 4 | 1, 5 | 2 # this is ok 6 | , 3 7 | ] 8 | -------------------------------------------------------------------------------- /testdata/unmarshal-array-6.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-array-6.toml 2 | 3 | ints = [ 4 | 1 # this is ok 5 | , 2 6 | , 3 7 | ] 8 | -------------------------------------------------------------------------------- /testdata/unmarshal-array-multitype.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-array-multitype.toml 2 | 3 | anys = [ 4 | "Foo Bar ", 5 | { name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" } 6 | ] 7 | -------------------------------------------------------------------------------- /testdata/unmarshal-arraytable-conflict-1.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-arraytable-conflict-1.toml 2 | 3 | [[fruit]] 4 | name = "apple" 5 | 6 | [[fruit.variety]] 7 | name = "red delicious" 8 | 9 | # This table conflicts with the previous table. 10 | [fruit.variety] 11 | name = "granny smith" 12 | -------------------------------------------------------------------------------- /testdata/unmarshal-arraytable-conflict-2.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-arraytable-conflict-2.toml 2 | 3 | [[fruit]] 4 | name = "apple" 5 | 6 | [fruit.variety] 7 | name = "granny smith" 8 | 9 | # This table conflicts with the previous table. 10 | [[fruit.variety]] 11 | name = "red delicious" 12 | -------------------------------------------------------------------------------- /testdata/unmarshal-arraytable-conflict-3.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-arraytable-conflict-3.toml 2 | 3 | [[fruit]] 4 | name = "apple" 5 | variety = { name = "granny smith" } 6 | 7 | # conflicts with inline table above 8 | [[fruit.variety]] 9 | -------------------------------------------------------------------------------- /testdata/unmarshal-arraytable-inline.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-arraytable-inline.toml 2 | 3 | products = [{name = "Hammer", sku = 738594937}, {}, 4 | {name = "Nail", sku = 284758393, color = "gray"}] -------------------------------------------------------------------------------- /testdata/unmarshal-arraytable-nested-1.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-arraytable-nested-1.toml 2 | 3 | [[fruit]] 4 | name = "apple" 5 | 6 | [fruit.physical] 7 | color = "red" 8 | shape = "round" 9 | 10 | [[fruit.variety]] 11 | name = "red delicious" 12 | 13 | [[fruit.variety]] 14 | name = "granny smith" 15 | 16 | [[fruit]] 17 | name = "banana" 18 | 19 | [fruit.physical] 20 | color = "yellow" 21 | shape = "lune" 22 | 23 | [[fruit.variety]] 24 | name = "plantain" 25 | -------------------------------------------------------------------------------- /testdata/unmarshal-arraytable-nested-2.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-arraytable-nested-2.toml 2 | 3 | [[fruit]] 4 | 5 | [[fruit.variety]] 6 | name = "red delicious" 7 | 8 | [[fruit.variety]] 9 | name = "granny smith" 10 | 11 | [[fruit]] 12 | 13 | [[fruit.variety]] 14 | name = "plantain" 15 | 16 | [[fruit.area]] 17 | name = "phillippines" 18 | -------------------------------------------------------------------------------- /testdata/unmarshal-arraytable-nested-3.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-arraytable-nested-3.toml 2 | 3 | [[fruit]] 4 | variety = [{name = "red delicious"}, {name = "granny smith"}] 5 | 6 | [[fruit]] 7 | variety = [{name = "plantain"}] 8 | area = [{name = "phillippines"}] 9 | -------------------------------------------------------------------------------- /testdata/unmarshal-arraytable.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-arraytable.toml 2 | 3 | [[products]] 4 | name = "Hammer" 5 | sku = 738594937 6 | 7 | [[products]] 8 | 9 | [[products]] 10 | name = "Nail" 11 | sku = 284758393 12 | color = "gray" 13 | -------------------------------------------------------------------------------- /testdata/unmarshal-errline-crlf.toml: -------------------------------------------------------------------------------- 1 | # This file has DOS-style line endings (CRLF). 2 | key1 = "hey" 3 | key2 = "ho" 4 | key3 = "lets" 5 | key2 = "go" 6 | 7 | # EOF. -------------------------------------------------------------------------------- /testdata/unmarshal-errline-lf.toml: -------------------------------------------------------------------------------- 1 | # This file has Unix-style line endings (LF). 2 | key1 = "hey" 3 | key2 = "ho" 4 | key3 = "lets" 5 | key2 = "go" 6 | 7 | # EOF. -------------------------------------------------------------------------------- /testdata/unmarshal-interface.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-interface.toml 2 | 3 | string = "string" 4 | int = 3 5 | float = 4.0 6 | boolean = true 7 | datetime = 1979-05-27T00:32:00.999999-07:00 8 | array = [1, 2, 3] 9 | inline = { key = "value" } 10 | 11 | [table] 12 | key = "value" 13 | 14 | [[arraytable]] 15 | key = "value" 16 | 17 | [[arraytable]] 18 | key = "value" 19 | -------------------------------------------------------------------------------- /testdata/unmarshal-pointer.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-pointer.toml 2 | 3 | inline = { key1 = "test", key2 = "a", key3 = "b" } 4 | 5 | [[tables]] 6 | key1 = "a" 7 | key2 = "a" 8 | key3 = "a" 9 | 10 | [[tables]] 11 | key1 = "b" 12 | key2 = "b" 13 | key3 = "b" 14 | -------------------------------------------------------------------------------- /testdata/unmarshal-string-1.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-string-1.toml 2 | 3 | key1 = "One\nTwo" 4 | key2 = """One\nTwo""" 5 | key3 = """ 6 | One 7 | Two""" 8 | -------------------------------------------------------------------------------- /testdata/unmarshal-string-2.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-string-2.toml 2 | # The following strings are byte-for-byte equivalent: 3 | 4 | key1 = "The quick brown fox jumps over the lazy dog." 5 | key2 = """ 6 | The quick brown \ 7 | 8 | 9 | fox jumps over \ 10 | the lazy dog.""" 11 | key3 = """\ 12 | The quick brown \ 13 | fox jumps over \ 14 | the lazy dog.\ 15 | """ 16 | -------------------------------------------------------------------------------- /testdata/unmarshal-string-3.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-string-3.toml 2 | 3 | winpath = 'C:\Users\nodejs\templates' 4 | winpath2 = '\\ServerX\admin$\system32\' 5 | quoted = 'Tom "Dubs" Preston-Werner' 6 | regex = '<\i\c*\s*>' 7 | -------------------------------------------------------------------------------- /testdata/unmarshal-string-4.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-string-4.toml 2 | 3 | regex2 = '''I [dw]on't need \d{2} apples''' 4 | lines = ''' 5 | The first newline is 6 | trimmed in raw strings. 7 | All other whitespace 8 | is preserved. 9 | ''' -------------------------------------------------------------------------------- /testdata/unmarshal-string-5.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-string-5.toml 2 | # This checks that quotes are valid in multiline strings. 3 | 4 | key1 = """I dare to say "this is valid TOML", and I'm not joking""" 5 | key2 = '''I dare to say 'this is valid TOML', and I'm not joking''' 6 | key3 = """I dare to say 'this is valid TOML', and I'm not joking""" 7 | -------------------------------------------------------------------------------- /testdata/unmarshal-string-6.toml: -------------------------------------------------------------------------------- 1 | # unmarshal-string-6.toml 2 | # This checks that raw tab characters are allowed in basic strings. 3 | 4 | key1 = " 0 && input[0] == '[' { 131 | var array []*value 132 | if err := json.Unmarshal(input, &array); err != nil { 133 | return err 134 | } 135 | *v = value{array: array} 136 | return nil 137 | } 138 | // It might be a primitive value. 139 | var prim prim 140 | if err := json.Unmarshal(input, &prim); err == nil { 141 | if prim.Type != "" { 142 | *v = value{prim: &prim} 143 | return nil 144 | } 145 | } 146 | // It's a table object. 147 | var table map[string]*value 148 | if err := json.Unmarshal(input, &table); err != nil { 149 | return err 150 | } 151 | *v = value{table: table} 152 | return nil 153 | } 154 | 155 | // This config turns off all table key remapping. 156 | var config = toml.Config{ 157 | NormFieldName: func(typ reflect.Type, keyOrField string) string { 158 | return keyOrField 159 | }, 160 | FieldToKey: func(typ reflect.Type, field string) string { 161 | return field 162 | }, 163 | WriteEmptyTables: true, 164 | } 165 | 166 | func main() { 167 | for _, arg := range os.Args[1:] { 168 | if arg == "-e" { 169 | encoder() 170 | return 171 | } 172 | } 173 | decoder() 174 | } 175 | 176 | func encoder() { 177 | var v map[string]*value // Top-level must be table! 178 | if err := json.NewDecoder(os.Stdin).Decode(&v); err != nil { 179 | fmt.Fprintln(os.Stderr, "Error in input JSON:", err) 180 | os.Exit(1) 181 | } 182 | if err := config.NewEncoder(os.Stdout).Encode(&v); err != nil { 183 | fmt.Fprintln(os.Stderr, err) 184 | os.Exit(1) 185 | } 186 | } 187 | 188 | func decoder() { 189 | var v value 190 | if err := config.NewDecoder(os.Stdin).Decode(&v); err != nil { 191 | fmt.Fprintln(os.Stderr, err) 192 | os.Exit(1) 193 | } 194 | if err := json.NewEncoder(os.Stdout).Encode(&v); err != nil { 195 | panic(err) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /toml-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script builds and runs the toml-test suite. 4 | # 5 | # Running all decoder tests: 6 | # 7 | # ./toml-test.sh 8 | # 9 | # Or encoder tests: 10 | # 11 | # ./toml-test.sh -encoder 12 | # 13 | # Or a specific encoder test: 14 | # 15 | # ./toml-test.sh -run valid/string/escapes -encoder 16 | 17 | set -e 18 | 19 | # Separate arguments. 20 | args="" 21 | testargs="" 22 | while [ $# -gt 0 ] ; do 23 | case "$1" in 24 | "-encoder") 25 | args="$args $1" 26 | testargs="$testargs -e" ;; 27 | "--") 28 | shift 29 | testargs="$testargs $@" 30 | break ;; 31 | *) 32 | args="$args $1" ;; 33 | esac 34 | shift 35 | done 36 | 37 | set -x 38 | 39 | # Build toml-test. 40 | version=2349618fe2bcc4393461c6c7d37b417c05e1b181 41 | env "GOBIN=$PWD" go install "github.com/BurntSushi/toml-test/cmd/toml-test@$version" 42 | 43 | # Build test adapter. 44 | go build -o toml-test-adapter toml-test-adapter.go 45 | 46 | # Run the tests. 47 | ./toml-test $args -- ./toml-test-adapter $testargs 48 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | const fieldTagName = "toml" 10 | 11 | // fieldCache maps normalized field names to their position in a struct. 12 | type fieldCache struct { 13 | named map[string]fieldInfo // fields with an explicit name in tag 14 | auto map[string]fieldInfo // fields with auto-assigned normalized names 15 | } 16 | 17 | type fieldInfo struct { 18 | index []int 19 | name string 20 | ignored bool 21 | } 22 | 23 | func makeFieldCache(cfg *Config, rt reflect.Type) fieldCache { 24 | named, auto := make(map[string]fieldInfo), make(map[string]fieldInfo) 25 | for i := 0; i < rt.NumField(); i++ { 26 | ft := rt.Field(i) 27 | // skip unexported fields 28 | if ft.PkgPath != "" && !ft.Anonymous { 29 | continue 30 | } 31 | col, _ := extractTag(ft.Tag.Get(fieldTagName)) 32 | info := fieldInfo{index: ft.Index, name: ft.Name, ignored: col == "-"} 33 | if col == "" || col == "-" { 34 | auto[cfg.NormFieldName(rt, ft.Name)] = info 35 | } else { 36 | named[col] = info 37 | } 38 | } 39 | return fieldCache{named, auto} 40 | } 41 | 42 | func (fc fieldCache) findField(cfg *Config, rv reflect.Value, name string) (reflect.Value, string, error) { 43 | info, found := fc.named[name] 44 | if !found { 45 | info, found = fc.auto[cfg.NormFieldName(rv.Type(), name)] 46 | } 47 | if !found { 48 | if cfg.MissingField == nil { 49 | return reflect.Value{}, "", fmt.Errorf("field corresponding to `%s' is not defined in %v", name, rv.Type()) 50 | } else { 51 | return reflect.Value{}, "", cfg.MissingField(rv.Type(), name) 52 | } 53 | } else if info.ignored { 54 | return reflect.Value{}, "", fmt.Errorf("field corresponding to `%s' in %v cannot be set through TOML", name, rv.Type()) 55 | } 56 | return rv.FieldByIndex(info.index), info.name, nil 57 | } 58 | 59 | func extractTag(tag string) (col, rest string) { 60 | tags := strings.SplitN(tag, ",", 2) 61 | if len(tags) == 2 { 62 | return strings.TrimSpace(tags[0]), strings.TrimSpace(tags[1]) 63 | } 64 | return strings.TrimSpace(tags[0]), "" 65 | } 66 | --------------------------------------------------------------------------------