mirror of https://github.com/ethereum/go-ethereum
cmd/geth: add --config file flag (#13875)
* p2p/discover, p2p/discv5: add marshaling methods to Node * p2p/netutil: make Netlist decodable from TOML * common/math: encode nil HexOrDecimal256 as 0x0 * cmd/geth: add --config file flag * cmd/geth: add missing license header * eth: prettify Config again, fix tests * eth: use gasprice.Config instead of duplicating its fields * eth/gasprice: hide nil default from dumpconfig output * cmd/geth: hide genesis block in dumpconfig output * node: make tests compile * console: fix tests * cmd/geth: make TOML keys look exactly like Go struct fields * p2p: use discovery by default This makes the zero Config slightly more useful. It also fixes package node tests because Node detects reuse of the datadir through the NodeDatabase. * cmd/geth: make ethstats URL settable through config file * cmd/faucet: fix configuration * cmd/geth: dedup attach tests * eth: add comment for DefaultConfig * eth: pass downloader.SyncMode in Config This removes the FastSync, LightSync flags in favour of a more general SyncMode flag. * cmd/utils: remove jitvm flags * cmd/utils: make mutually exclusive flag error prettier It now reads: Fatal: flags --dev, --testnet can't be used at the same time * p2p: fix typo * node: add DefaultConfig, use it for geth * mobile: add missing NoDiscovery option * cmd/utils: drop MakeNode This exposed a couple of places that needed to be updated to use node.DefaultConfig. * node: fix typo * eth: make fast sync the default mode * cmd/utils: remove IPCApiFlag (unused) * node: remove default IPC path Set it in the frontends instead. * cmd/geth: add --syncmode * cmd/utils: make --ipcdisable and --ipcpath mutually exclusive * cmd/utils: don't enable WS, HTTP when setting addr * cmd/utils: fix --identitypull/14323/head
parent
b57680b0b2
commit
30d706c35e
@ -0,0 +1,186 @@ |
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"bufio" |
||||
"encoding/hex" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"os" |
||||
"reflect" |
||||
"unicode" |
||||
|
||||
cli "gopkg.in/urfave/cli.v1" |
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils" |
||||
"github.com/ethereum/go-ethereum/contracts/release" |
||||
"github.com/ethereum/go-ethereum/eth" |
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/params" |
||||
"github.com/naoina/toml" |
||||
) |
||||
|
||||
var ( |
||||
dumpConfigCommand = cli.Command{ |
||||
Action: dumpConfig, |
||||
Name: "dumpconfig", |
||||
Usage: "Show configuration values", |
||||
ArgsUsage: "", |
||||
Category: "MISCELLANEOUS COMMANDS", |
||||
Description: `The dumpconfig command shows configuration values.`, |
||||
} |
||||
|
||||
configFileFlag = cli.StringFlag{ |
||||
Name: "config", |
||||
Usage: "TOML configuration file", |
||||
} |
||||
) |
||||
|
||||
// These settings ensure that TOML keys use the same names as Go struct fields.
|
||||
var tomlSettings = toml.Config{ |
||||
NormFieldName: func(rt reflect.Type, key string) string { |
||||
return key |
||||
}, |
||||
FieldToKey: func(rt reflect.Type, field string) string { |
||||
return field |
||||
}, |
||||
MissingField: func(rt reflect.Type, field string) error { |
||||
link := "" |
||||
if unicode.IsUpper(rune(rt.Name()[0])) && rt.PkgPath() != "main" { |
||||
link = fmt.Sprintf(", see https://godoc.org/%s#%s for available fields", rt.PkgPath(), rt.Name()) |
||||
} |
||||
return fmt.Errorf("field '%s' is not defined in %s%s", field, rt.String(), link) |
||||
}, |
||||
} |
||||
|
||||
type ethstatsConfig struct { |
||||
URL string `toml:",omitempty"` |
||||
} |
||||
|
||||
type gethConfig struct { |
||||
Eth eth.Config |
||||
Node node.Config |
||||
Ethstats ethstatsConfig |
||||
} |
||||
|
||||
func loadConfig(file string, cfg *gethConfig) error { |
||||
f, err := os.Open(file) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer f.Close() |
||||
|
||||
err = tomlSettings.NewDecoder(bufio.NewReader(f)).Decode(cfg) |
||||
// Add file name to errors that have a line number.
|
||||
if _, ok := err.(*toml.LineError); ok { |
||||
err = errors.New(file + ", " + err.Error()) |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func defaultNodeConfig() node.Config { |
||||
cfg := node.DefaultConfig |
||||
cfg.Name = clientIdentifier |
||||
cfg.Version = params.VersionWithCommit(gitCommit) |
||||
cfg.HTTPModules = append(cfg.HTTPModules, "eth") |
||||
cfg.WSModules = append(cfg.WSModules, "eth") |
||||
cfg.IPCPath = "geth.ipc" |
||||
return cfg |
||||
} |
||||
|
||||
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { |
||||
// Load defaults.
|
||||
cfg := gethConfig{ |
||||
Eth: eth.DefaultConfig, |
||||
Node: defaultNodeConfig(), |
||||
} |
||||
|
||||
// Load config file.
|
||||
if file := ctx.GlobalString(configFileFlag.Name); file != "" { |
||||
if err := loadConfig(file, &cfg); err != nil { |
||||
utils.Fatalf("%v", err) |
||||
} |
||||
} |
||||
|
||||
// Apply flags.
|
||||
utils.SetNodeConfig(ctx, &cfg.Node) |
||||
stack, err := node.New(&cfg.Node) |
||||
if err != nil { |
||||
utils.Fatalf("Failed to create the protocol stack: %v", err) |
||||
} |
||||
utils.SetEthConfig(ctx, stack, &cfg.Eth) |
||||
if ctx.GlobalIsSet(utils.EthStatsURLFlag.Name) { |
||||
cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name) |
||||
} |
||||
|
||||
return stack, cfg |
||||
} |
||||
|
||||
func makeFullNode(ctx *cli.Context) *node.Node { |
||||
stack, cfg := makeConfigNode(ctx) |
||||
|
||||
utils.RegisterEthService(stack, &cfg.Eth) |
||||
|
||||
// Whisper must be explicitly enabled, but is auto-enabled in --dev mode.
|
||||
shhEnabled := ctx.GlobalBool(utils.WhisperEnabledFlag.Name) |
||||
shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DevModeFlag.Name) |
||||
if shhEnabled || shhAutoEnabled { |
||||
utils.RegisterShhService(stack) |
||||
} |
||||
|
||||
// Add the Ethereum Stats daemon if requested.
|
||||
if cfg.Ethstats.URL != "" { |
||||
utils.RegisterEthStatsService(stack, cfg.Ethstats.URL) |
||||
} |
||||
|
||||
// Add the release oracle service so it boots along with node.
|
||||
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { |
||||
config := release.Config{ |
||||
Oracle: relOracle, |
||||
Major: uint32(params.VersionMajor), |
||||
Minor: uint32(params.VersionMinor), |
||||
Patch: uint32(params.VersionPatch), |
||||
} |
||||
commit, _ := hex.DecodeString(gitCommit) |
||||
copy(config.Commit[:], commit) |
||||
return release.NewReleaseService(ctx, config) |
||||
}); err != nil { |
||||
utils.Fatalf("Failed to register the Geth release oracle service: %v", err) |
||||
} |
||||
return stack |
||||
} |
||||
|
||||
// dumpConfig is the dumpconfig command.
|
||||
func dumpConfig(ctx *cli.Context) error { |
||||
_, cfg := makeConfigNode(ctx) |
||||
comment := "" |
||||
|
||||
if cfg.Eth.Genesis != nil { |
||||
cfg.Eth.Genesis = nil |
||||
comment += "# Note: this config doesn't contain the genesis block.\n\n" |
||||
} |
||||
|
||||
out, err := tomlSettings.Marshal(&cfg) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
io.WriteString(os.Stdout, comment) |
||||
os.Stdout.Write(out) |
||||
return nil |
||||
} |
@ -0,0 +1,117 @@ |
||||
// Copyright 2014 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package eth |
||||
|
||||
import ( |
||||
"math/big" |
||||
"os" |
||||
"os/user" |
||||
"path/filepath" |
||||
"runtime" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/core" |
||||
"github.com/ethereum/go-ethereum/eth/downloader" |
||||
"github.com/ethereum/go-ethereum/eth/gasprice" |
||||
"github.com/ethereum/go-ethereum/params" |
||||
) |
||||
|
||||
// DefaultConfig contains default settings for use on the Ethereum main net.
|
||||
var DefaultConfig = Config{ |
||||
SyncMode: downloader.FastSync, |
||||
EthashCachesInMem: 2, |
||||
EthashCachesOnDisk: 3, |
||||
EthashDatasetsInMem: 1, |
||||
EthashDatasetsOnDisk: 2, |
||||
NetworkId: 1, |
||||
LightPeers: 20, |
||||
DatabaseCache: 128, |
||||
GasPrice: big.NewInt(20 * params.Shannon), |
||||
|
||||
GPO: gasprice.Config{ |
||||
Blocks: 10, |
||||
Percentile: 50, |
||||
}, |
||||
} |
||||
|
||||
func init() { |
||||
home := os.Getenv("HOME") |
||||
if home == "" { |
||||
if user, err := user.Current(); err == nil { |
||||
home = user.HomeDir |
||||
} |
||||
} |
||||
if runtime.GOOS == "windows" { |
||||
DefaultConfig.EthashDatasetDir = filepath.Join(home, "AppData", "Ethash") |
||||
} else { |
||||
DefaultConfig.EthashDatasetDir = filepath.Join(home, ".ethash") |
||||
} |
||||
} |
||||
|
||||
//go:generate gencodec -type Config -field-override configMarshaling -formats toml -out gen_config.go
|
||||
|
||||
type Config struct { |
||||
// The genesis block, which is inserted if the database is empty.
|
||||
// If nil, the Ethereum main net block is used.
|
||||
Genesis *core.Genesis `toml:",omitempty"` |
||||
|
||||
// Protocol options
|
||||
NetworkId int // Network ID to use for selecting peers to connect to
|
||||
SyncMode downloader.SyncMode |
||||
|
||||
// Light client options
|
||||
LightServ int `toml:",omitempty"` // Maximum percentage of time allowed for serving LES requests
|
||||
LightPeers int `toml:",omitempty"` // Maximum number of LES client peers
|
||||
MaxPeers int `toml:"-"` // Maximum number of global peers
|
||||
|
||||
// Database options
|
||||
SkipBcVersionCheck bool `toml:"-"` |
||||
DatabaseHandles int `toml:"-"` |
||||
DatabaseCache int |
||||
|
||||
// Mining-related options
|
||||
Etherbase common.Address `toml:",omitempty"` |
||||
MinerThreads int `toml:",omitempty"` |
||||
ExtraData []byte `toml:",omitempty"` |
||||
GasPrice *big.Int |
||||
|
||||
// Ethash options
|
||||
EthashCacheDir string |
||||
EthashCachesInMem int |
||||
EthashCachesOnDisk int |
||||
EthashDatasetDir string |
||||
EthashDatasetsInMem int |
||||
EthashDatasetsOnDisk int |
||||
|
||||
// Gas Price Oracle options
|
||||
GPO gasprice.Config |
||||
|
||||
// Enables tracking of SHA3 preimages in the VM
|
||||
EnablePreimageRecording bool |
||||
|
||||
// Miscellaneous options
|
||||
SolcPath string |
||||
DocRoot string `toml:"-"` |
||||
PowFake bool `toml:"-"` |
||||
PowTest bool `toml:"-"` |
||||
PowShared bool `toml:"-"` |
||||
} |
||||
|
||||
type configMarshaling struct { |
||||
ExtraData hexutil.Bytes |
||||
} |
@ -0,0 +1,186 @@ |
||||
// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
|
||||
|
||||
package eth |
||||
|
||||
import ( |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/core" |
||||
"github.com/ethereum/go-ethereum/eth/downloader" |
||||
"github.com/ethereum/go-ethereum/eth/gasprice" |
||||
) |
||||
|
||||
func (c Config) MarshalTOML() (interface{}, error) { |
||||
type Config struct { |
||||
Genesis *core.Genesis `toml:",omitempty"` |
||||
NetworkId int |
||||
SyncMode downloader.SyncMode |
||||
LightServ int `toml:",omitempty"` |
||||
LightPeers int `toml:",omitempty"` |
||||
MaxPeers int `toml:"-"` |
||||
SkipBcVersionCheck bool `toml:"-"` |
||||
DatabaseHandles int `toml:"-"` |
||||
DatabaseCache int |
||||
Etherbase common.Address `toml:",omitempty"` |
||||
MinerThreads int `toml:",omitempty"` |
||||
ExtraData hexutil.Bytes `toml:",omitempty"` |
||||
GasPrice *big.Int |
||||
EthashCacheDir string |
||||
EthashCachesInMem int |
||||
EthashCachesOnDisk int |
||||
EthashDatasetDir string |
||||
EthashDatasetsInMem int |
||||
EthashDatasetsOnDisk int |
||||
GPO gasprice.Config |
||||
EnablePreimageRecording bool |
||||
SolcPath string |
||||
DocRoot string `toml:"-"` |
||||
PowFake bool `toml:"-"` |
||||
PowTest bool `toml:"-"` |
||||
PowShared bool `toml:"-"` |
||||
} |
||||
var enc Config |
||||
enc.Genesis = c.Genesis |
||||
enc.NetworkId = c.NetworkId |
||||
enc.SyncMode = c.SyncMode |
||||
enc.LightServ = c.LightServ |
||||
enc.LightPeers = c.LightPeers |
||||
enc.MaxPeers = c.MaxPeers |
||||
enc.SkipBcVersionCheck = c.SkipBcVersionCheck |
||||
enc.DatabaseHandles = c.DatabaseHandles |
||||
enc.DatabaseCache = c.DatabaseCache |
||||
enc.Etherbase = c.Etherbase |
||||
enc.MinerThreads = c.MinerThreads |
||||
enc.ExtraData = c.ExtraData |
||||
enc.GasPrice = c.GasPrice |
||||
enc.EthashCacheDir = c.EthashCacheDir |
||||
enc.EthashCachesInMem = c.EthashCachesInMem |
||||
enc.EthashCachesOnDisk = c.EthashCachesOnDisk |
||||
enc.EthashDatasetDir = c.EthashDatasetDir |
||||
enc.EthashDatasetsInMem = c.EthashDatasetsInMem |
||||
enc.EthashDatasetsOnDisk = c.EthashDatasetsOnDisk |
||||
enc.GPO = c.GPO |
||||
enc.EnablePreimageRecording = c.EnablePreimageRecording |
||||
enc.SolcPath = c.SolcPath |
||||
enc.DocRoot = c.DocRoot |
||||
enc.PowFake = c.PowFake |
||||
enc.PowTest = c.PowTest |
||||
enc.PowShared = c.PowShared |
||||
return &enc, nil |
||||
} |
||||
|
||||
func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { |
||||
type Config struct { |
||||
Genesis *core.Genesis `toml:",omitempty"` |
||||
NetworkId *int |
||||
SyncMode *downloader.SyncMode |
||||
LightServ *int `toml:",omitempty"` |
||||
LightPeers *int `toml:",omitempty"` |
||||
MaxPeers *int `toml:"-"` |
||||
SkipBcVersionCheck *bool `toml:"-"` |
||||
DatabaseHandles *int `toml:"-"` |
||||
DatabaseCache *int |
||||
Etherbase *common.Address `toml:",omitempty"` |
||||
MinerThreads *int `toml:",omitempty"` |
||||
ExtraData hexutil.Bytes `toml:",omitempty"` |
||||
GasPrice *big.Int |
||||
EthashCacheDir *string |
||||
EthashCachesInMem *int |
||||
EthashCachesOnDisk *int |
||||
EthashDatasetDir *string |
||||
EthashDatasetsInMem *int |
||||
EthashDatasetsOnDisk *int |
||||
GPO *gasprice.Config |
||||
EnablePreimageRecording *bool |
||||
SolcPath *string |
||||
DocRoot *string `toml:"-"` |
||||
PowFake *bool `toml:"-"` |
||||
PowTest *bool `toml:"-"` |
||||
PowShared *bool `toml:"-"` |
||||
} |
||||
var dec Config |
||||
if err := unmarshal(&dec); err != nil { |
||||
return err |
||||
} |
||||
if dec.Genesis != nil { |
||||
c.Genesis = dec.Genesis |
||||
} |
||||
if dec.NetworkId != nil { |
||||
c.NetworkId = *dec.NetworkId |
||||
} |
||||
if dec.SyncMode != nil { |
||||
c.SyncMode = *dec.SyncMode |
||||
} |
||||
if dec.LightServ != nil { |
||||
c.LightServ = *dec.LightServ |
||||
} |
||||
if dec.LightPeers != nil { |
||||
c.LightPeers = *dec.LightPeers |
||||
} |
||||
if dec.MaxPeers != nil { |
||||
c.MaxPeers = *dec.MaxPeers |
||||
} |
||||
if dec.SkipBcVersionCheck != nil { |
||||
c.SkipBcVersionCheck = *dec.SkipBcVersionCheck |
||||
} |
||||
if dec.DatabaseHandles != nil { |
||||
c.DatabaseHandles = *dec.DatabaseHandles |
||||
} |
||||
if dec.DatabaseCache != nil { |
||||
c.DatabaseCache = *dec.DatabaseCache |
||||
} |
||||
if dec.Etherbase != nil { |
||||
c.Etherbase = *dec.Etherbase |
||||
} |
||||
if dec.MinerThreads != nil { |
||||
c.MinerThreads = *dec.MinerThreads |
||||
} |
||||
if dec.ExtraData != nil { |
||||
c.ExtraData = dec.ExtraData |
||||
} |
||||
if dec.GasPrice != nil { |
||||
c.GasPrice = dec.GasPrice |
||||
} |
||||
if dec.EthashCacheDir != nil { |
||||
c.EthashCacheDir = *dec.EthashCacheDir |
||||
} |
||||
if dec.EthashCachesInMem != nil { |
||||
c.EthashCachesInMem = *dec.EthashCachesInMem |
||||
} |
||||
if dec.EthashCachesOnDisk != nil { |
||||
c.EthashCachesOnDisk = *dec.EthashCachesOnDisk |
||||
} |
||||
if dec.EthashDatasetDir != nil { |
||||
c.EthashDatasetDir = *dec.EthashDatasetDir |
||||
} |
||||
if dec.EthashDatasetsInMem != nil { |
||||
c.EthashDatasetsInMem = *dec.EthashDatasetsInMem |
||||
} |
||||
if dec.EthashDatasetsOnDisk != nil { |
||||
c.EthashDatasetsOnDisk = *dec.EthashDatasetsOnDisk |
||||
} |
||||
if dec.GPO != nil { |
||||
c.GPO = *dec.GPO |
||||
} |
||||
if dec.EnablePreimageRecording != nil { |
||||
c.EnablePreimageRecording = *dec.EnablePreimageRecording |
||||
} |
||||
if dec.SolcPath != nil { |
||||
c.SolcPath = *dec.SolcPath |
||||
} |
||||
if dec.DocRoot != nil { |
||||
c.DocRoot = *dec.DocRoot |
||||
} |
||||
if dec.PowFake != nil { |
||||
c.PowFake = *dec.PowFake |
||||
} |
||||
if dec.PowTest != nil { |
||||
c.PowTest = *dec.PowTest |
||||
} |
||||
if dec.PowShared != nil { |
||||
c.PowShared = *dec.PowShared |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,19 @@ |
||||
Copyright (c) 2015 Naoya Inada <naoina@kuune.org> |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in |
||||
all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
THE SOFTWARE. |
@ -0,0 +1,13 @@ |
||||
# stringutil [![Build Status](https://travis-ci.org/naoina/go-stringutil.svg?branch=master)](https://travis-ci.org/naoina/go-stringutil) |
||||
|
||||
## Installation |
||||
|
||||
go get -u github.com/naoina/go-stringutil |
||||
|
||||
## Documentation |
||||
|
||||
See https://godoc.org/github.com/naoina/go-stringutil |
||||
|
||||
## License |
||||
|
||||
MIT |
@ -0,0 +1,253 @@ |
||||
package stringutil |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sort" |
||||
"unicode/utf8" |
||||
) |
||||
|
||||
const ( |
||||
terminationCharacter = '#' |
||||
) |
||||
|
||||
func mustDoubleArray(da *doubleArray, err error) *doubleArray { |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return da |
||||
} |
||||
|
||||
func (da *doubleArray) Build(keys []string) error { |
||||
records := makeRecords(keys) |
||||
if err := da.build(records, 1, 0, make(map[int]struct{})); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
type doubleArray struct { |
||||
bc []baseCheck |
||||
node []int |
||||
} |
||||
|
||||
func newDoubleArray(keys []string) (*doubleArray, error) { |
||||
da := &doubleArray{ |
||||
bc: []baseCheck{0}, |
||||
node: []int{-1}, // A start index is adjusting to 1 because 0 will be used as a mark of non-existent node.
|
||||
} |
||||
if err := da.Build(keys); err != nil { |
||||
return nil, err |
||||
} |
||||
return da, nil |
||||
} |
||||
|
||||
// baseCheck contains BASE, CHECK and Extra flags.
|
||||
// From the top, 22bits of BASE, 2bits of Extra flags and 8bits of CHECK.
|
||||
//
|
||||
// BASE (22bit) | Extra flags (2bit) | CHECK (8bit)
|
||||
// |----------------------|--|--------|
|
||||
// 32 10 8 0
|
||||
type baseCheck uint32 |
||||
|
||||
func (bc baseCheck) Base() int { |
||||
return int(bc >> 10) |
||||
} |
||||
|
||||
func (bc *baseCheck) SetBase(base int) { |
||||
*bc |= baseCheck(base) << 10 |
||||
} |
||||
|
||||
func (bc baseCheck) Check() byte { |
||||
return byte(bc) |
||||
} |
||||
|
||||
func (bc *baseCheck) SetCheck(check byte) { |
||||
*bc |= baseCheck(check) |
||||
} |
||||
|
||||
func (bc baseCheck) IsEmpty() bool { |
||||
return bc&0xfffffcff == 0 |
||||
} |
||||
|
||||
func (da *doubleArray) Lookup(path string) (length int) { |
||||
idx := 1 |
||||
tmpIdx := idx |
||||
for i := 0; i < len(path); i++ { |
||||
c := path[i] |
||||
tmpIdx = da.nextIndex(da.bc[tmpIdx].Base(), c) |
||||
if tmpIdx >= len(da.bc) || da.bc[tmpIdx].Check() != c { |
||||
break |
||||
} |
||||
idx = tmpIdx |
||||
} |
||||
if next := da.nextIndex(da.bc[idx].Base(), terminationCharacter); next < len(da.bc) && da.bc[next].Check() == terminationCharacter { |
||||
return da.node[da.bc[next].Base()] |
||||
} |
||||
return -1 |
||||
} |
||||
|
||||
func (da *doubleArray) LookupByBytes(path []byte) (length int) { |
||||
idx := 1 |
||||
tmpIdx := idx |
||||
for i := 0; i < len(path); i++ { |
||||
c := path[i] |
||||
tmpIdx = da.nextIndex(da.bc[tmpIdx].Base(), c) |
||||
if tmpIdx >= len(da.bc) || da.bc[tmpIdx].Check() != c { |
||||
break |
||||
} |
||||
idx = tmpIdx |
||||
} |
||||
if next := da.nextIndex(da.bc[idx].Base(), terminationCharacter); next < len(da.bc) && da.bc[next].Check() == terminationCharacter { |
||||
return da.node[da.bc[next].Base()] |
||||
} |
||||
return -1 |
||||
} |
||||
|
||||
func (da *doubleArray) build(srcs []record, idx, depth int, usedBase map[int]struct{}) error { |
||||
sort.Stable(recordSlice(srcs)) |
||||
base, siblings, leaf, err := da.arrange(srcs, idx, depth, usedBase) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if leaf != nil { |
||||
da.bc[idx].SetBase(len(da.node)) |
||||
da.node = append(da.node, leaf.value) |
||||
} |
||||
for _, sib := range siblings { |
||||
da.setCheck(da.nextIndex(base, sib.c), sib.c) |
||||
} |
||||
for _, sib := range siblings { |
||||
if err := da.build(srcs[sib.start:sib.end], da.nextIndex(base, sib.c), depth+1, usedBase); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (da *doubleArray) setBase(i, base int) { |
||||
da.bc[i].SetBase(base) |
||||
} |
||||
|
||||
func (da *doubleArray) setCheck(i int, check byte) { |
||||
da.bc[i].SetCheck(check) |
||||
} |
||||
|
||||
func (da *doubleArray) findEmptyIndex(start int) int { |
||||
i := start |
||||
for ; i < len(da.bc); i++ { |
||||
if da.bc[i].IsEmpty() { |
||||
break |
||||
} |
||||
} |
||||
return i |
||||
} |
||||
|
||||
// findBase returns good BASE.
|
||||
func (da *doubleArray) findBase(siblings []sibling, start int, usedBase map[int]struct{}) (base int) { |
||||
for idx, firstChar := start+1, siblings[0].c; ; idx = da.findEmptyIndex(idx + 1) { |
||||
base = da.nextIndex(idx, firstChar) |
||||
if _, used := usedBase[base]; used { |
||||
continue |
||||
} |
||||
i := 0 |
||||
for ; i < len(siblings); i++ { |
||||
next := da.nextIndex(base, siblings[i].c) |
||||
if len(da.bc) <= next { |
||||
da.bc = append(da.bc, make([]baseCheck, next-len(da.bc)+1)...) |
||||
} |
||||
if !da.bc[next].IsEmpty() { |
||||
break |
||||
} |
||||
} |
||||
if i == len(siblings) { |
||||
break |
||||
} |
||||
} |
||||
usedBase[base] = struct{}{} |
||||
return base |
||||
} |
||||
|
||||
func (da *doubleArray) arrange(records []record, idx, depth int, usedBase map[int]struct{}) (base int, siblings []sibling, leaf *record, err error) { |
||||
siblings, leaf, err = makeSiblings(records, depth) |
||||
if err != nil { |
||||
return -1, nil, nil, err |
||||
} |
||||
if len(siblings) < 1 { |
||||
return -1, nil, leaf, nil |
||||
} |
||||
base = da.findBase(siblings, idx, usedBase) |
||||
da.setBase(idx, base) |
||||
return base, siblings, leaf, err |
||||
} |
||||
|
||||
type sibling struct { |
||||
start int |
||||
end int |
||||
c byte |
||||
} |
||||
|
||||
func (da *doubleArray) nextIndex(base int, c byte) int { |
||||
return base ^ int(c) |
||||
} |
||||
|
||||
func makeSiblings(records []record, depth int) (sib []sibling, leaf *record, err error) { |
||||
var ( |
||||
pc byte |
||||
n int |
||||
) |
||||
for i, r := range records { |
||||
if len(r.key) <= depth { |
||||
leaf = &r |
||||
continue |
||||
} |
||||
c := r.key[depth] |
||||
switch { |
||||
case pc < c: |
||||
sib = append(sib, sibling{start: i, c: c}) |
||||
case pc == c: |
||||
continue |
||||
default: |
||||
return nil, nil, fmt.Errorf("stringutil: BUG: records hasn't been sorted") |
||||
} |
||||
if n > 0 { |
||||
sib[n-1].end = i |
||||
} |
||||
pc = c |
||||
n++ |
||||
} |
||||
if n == 0 { |
||||
return nil, leaf, nil |
||||
} |
||||
sib[n-1].end = len(records) |
||||
return sib, leaf, nil |
||||
} |
||||
|
||||
type record struct { |
||||
key string |
||||
value int |
||||
} |
||||
|
||||
func makeRecords(srcs []string) (records []record) { |
||||
termChar := string(terminationCharacter) |
||||
for _, s := range srcs { |
||||
records = append(records, record{ |
||||
key: string(s + termChar), |
||||
value: utf8.RuneCountInString(s), |
||||
}) |
||||
} |
||||
return records |
||||
} |
||||
|
||||
type recordSlice []record |
||||
|
||||
func (rs recordSlice) Len() int { |
||||
return len(rs) |
||||
} |
||||
|
||||
func (rs recordSlice) Less(i, j int) bool { |
||||
return rs[i].key < rs[j].key |
||||
} |
||||
|
||||
func (rs recordSlice) Swap(i, j int) { |
||||
rs[i], rs[j] = rs[j], rs[i] |
||||
} |
@ -0,0 +1,320 @@ |
||||
package stringutil |
||||
|
||||
import ( |
||||
"sync" |
||||
"unicode" |
||||
"unicode/utf8" |
||||
) |
||||
|
||||
var ( |
||||
mu sync.Mutex |
||||
|
||||
// Based on https://github.com/golang/lint/blob/32a87160691b3c96046c0c678fe57c5bef761456/lint.go#L702
|
||||
commonInitialismMap = map[string]struct{}{ |
||||
"API": struct{}{}, |
||||
"ASCII": struct{}{}, |
||||
"CPU": struct{}{}, |
||||
"CSRF": struct{}{}, |
||||
"CSS": struct{}{}, |
||||
"DNS": struct{}{}, |
||||
"EOF": struct{}{}, |
||||
"GUID": struct{}{}, |
||||
"HTML": struct{}{}, |
||||
"HTTP": struct{}{}, |
||||
"HTTPS": struct{}{}, |
||||
"ID": struct{}{}, |
||||
"IP": struct{}{}, |
||||
"JSON": struct{}{}, |
||||
"LHS": struct{}{}, |
||||
"QPS": struct{}{}, |
||||
"RAM": struct{}{}, |
||||
"RHS": struct{}{}, |
||||
"RPC": struct{}{}, |
||||
"SLA": struct{}{}, |
||||
"SMTP": struct{}{}, |
||||
"SQL": struct{}{}, |
||||
"SSH": struct{}{}, |
||||
"TCP": struct{}{}, |
||||
"TLS": struct{}{}, |
||||
"TTL": struct{}{}, |
||||
"UDP": struct{}{}, |
||||
"UI": struct{}{}, |
||||
"UID": struct{}{}, |
||||
"UUID": struct{}{}, |
||||
"URI": struct{}{}, |
||||
"URL": struct{}{}, |
||||
"UTF8": struct{}{}, |
||||
"VM": struct{}{}, |
||||
"XML": struct{}{}, |
||||
"XSRF": struct{}{}, |
||||
"XSS": struct{}{}, |
||||
} |
||||
commonInitialisms = keys(commonInitialismMap) |
||||
commonInitialism = mustDoubleArray(newDoubleArray(commonInitialisms)) |
||||
longestLen = longestLength(commonInitialisms) |
||||
shortestLen = shortestLength(commonInitialisms, longestLen) |
||||
) |
||||
|
||||
// ToUpperCamelCase returns a copy of the string s with all Unicode letters mapped to their camel case.
|
||||
// It will convert to upper case previous letter of '_' and first letter, and remove letter of '_'.
|
||||
func ToUpperCamelCase(s string) string { |
||||
if s == "" { |
||||
return "" |
||||
} |
||||
upper := true |
||||
start := 0 |
||||
result := make([]byte, 0, len(s)) |
||||
var runeBuf [utf8.UTFMax]byte |
||||
var initialism []byte |
||||
for _, c := range s { |
||||
if c == '_' { |
||||
upper = true |
||||
candidate := string(result[start:]) |
||||
initialism = initialism[:0] |
||||
for _, r := range candidate { |
||||
if r < utf8.RuneSelf { |
||||
initialism = append(initialism, toUpperASCII(byte(r))) |
||||
} else { |
||||
n := utf8.EncodeRune(runeBuf[:], unicode.ToUpper(r)) |
||||
initialism = append(initialism, runeBuf[:n]...) |
||||
} |
||||
} |
||||
if length := commonInitialism.LookupByBytes(initialism); length > 0 { |
||||
result = append(result[:start], initialism...) |
||||
} |
||||
start = len(result) |
||||
continue |
||||
} |
||||
if upper { |
||||
if c < utf8.RuneSelf { |
||||
result = append(result, toUpperASCII(byte(c))) |
||||
} else { |
||||
n := utf8.EncodeRune(runeBuf[:], unicode.ToUpper(c)) |
||||
result = append(result, runeBuf[:n]...) |
||||
} |
||||
upper = false |
||||
continue |
||||
} |
||||
if c < utf8.RuneSelf { |
||||
result = append(result, byte(c)) |
||||
} else { |
||||
n := utf8.EncodeRune(runeBuf[:], c) |
||||
result = append(result, runeBuf[:n]...) |
||||
} |
||||
} |
||||
candidate := string(result[start:]) |
||||
initialism = initialism[:0] |
||||
for _, r := range candidate { |
||||
if r < utf8.RuneSelf { |
||||
initialism = append(initialism, toUpperASCII(byte(r))) |
||||
} else { |
||||
n := utf8.EncodeRune(runeBuf[:], unicode.ToUpper(r)) |
||||
initialism = append(initialism, runeBuf[:n]...) |
||||
} |
||||
} |
||||
if length := commonInitialism.LookupByBytes(initialism); length > 0 { |
||||
result = append(result[:start], initialism...) |
||||
} |
||||
return string(result) |
||||
} |
||||
|
||||
// ToUpperCamelCaseASCII is similar to ToUpperCamelCase, but optimized for
|
||||
// only the ASCII characters.
|
||||
// ToUpperCamelCaseASCII is faster than ToUpperCamelCase, but doesn't work if
|
||||
// contains non-ASCII characters.
|
||||
func ToUpperCamelCaseASCII(s string) string { |
||||
if s == "" { |
||||
return "" |
||||
} |
||||
upper := true |
||||
start := 0 |
||||
result := make([]byte, 0, len(s)) |
||||
var initialism []byte |
||||
for i := 0; i < len(s); i++ { |
||||
c := s[i] |
||||
if c == '_' { |
||||
upper = true |
||||
candidate := result[start:] |
||||
initialism = initialism[:0] |
||||
for _, b := range candidate { |
||||
initialism = append(initialism, toUpperASCII(b)) |
||||
} |
||||
if length := commonInitialism.LookupByBytes(initialism); length > 0 { |
||||
result = append(result[:start], initialism...) |
||||
} |
||||
start = len(result) |
||||
continue |
||||
} |
||||
if upper { |
||||
result = append(result, toUpperASCII(c)) |
||||
upper = false |
||||
continue |
||||
} |
||||
result = append(result, c) |
||||
} |
||||
candidate := result[start:] |
||||
initialism = initialism[:0] |
||||
for _, b := range candidate { |
||||
initialism = append(initialism, toUpperASCII(b)) |
||||
} |
||||
if length := commonInitialism.LookupByBytes(initialism); length > 0 { |
||||
result = append(result[:start], initialism...) |
||||
} |
||||
return string(result) |
||||
} |
||||
|
||||
// ToSnakeCase returns a copy of the string s with all Unicode letters mapped to their snake case.
|
||||
// It will insert letter of '_' at position of previous letter of uppercase and all
|
||||
// letters convert to lower case.
|
||||
// ToSnakeCase does not insert '_' letter into a common initialism word like ID, URL and so on.
|
||||
func ToSnakeCase(s string) string { |
||||
if s == "" { |
||||
return "" |
||||
} |
||||
result := make([]byte, 0, len(s)) |
||||
var runeBuf [utf8.UTFMax]byte |
||||
var j, skipCount int |
||||
for i, c := range s { |
||||
if i < skipCount { |
||||
continue |
||||
} |
||||
if unicode.IsUpper(c) { |
||||
if i != 0 { |
||||
result = append(result, '_') |
||||
} |
||||
next := nextIndex(j, len(s)) |
||||
if length := commonInitialism.Lookup(s[j:next]); length > 0 { |
||||
for _, r := range s[j : j+length] { |
||||
if r < utf8.RuneSelf { |
||||
result = append(result, toLowerASCII(byte(r))) |
||||
} else { |
||||
n := utf8.EncodeRune(runeBuf[:], unicode.ToLower(r)) |
||||
result = append(result, runeBuf[:n]...) |
||||
} |
||||
} |
||||
j += length - 1 |
||||
skipCount = i + length |
||||
continue |
||||
} |
||||
} |
||||
if c < utf8.RuneSelf { |
||||
result = append(result, toLowerASCII(byte(c))) |
||||
} else { |
||||
n := utf8.EncodeRune(runeBuf[:], unicode.ToLower(c)) |
||||
result = append(result, runeBuf[:n]...) |
||||
} |
||||
j++ |
||||
} |
||||
return string(result) |
||||
} |
||||
|
||||
// ToSnakeCaseASCII is similar to ToSnakeCase, but optimized for only the ASCII
|
||||
// characters.
|
||||
// ToSnakeCaseASCII is faster than ToSnakeCase, but doesn't work correctly if
|
||||
// contains non-ASCII characters.
|
||||
func ToSnakeCaseASCII(s string) string { |
||||
if s == "" { |
||||
return "" |
||||
} |
||||
result := make([]byte, 0, len(s)) |
||||
for i := 0; i < len(s); i++ { |
||||
c := s[i] |
||||
if isUpperASCII(c) { |
||||
if i != 0 { |
||||
result = append(result, '_') |
||||
} |
||||
if k := i + shortestLen - 1; k < len(s) && isUpperASCII(s[k]) { |
||||
if length := commonInitialism.Lookup(s[i:nextIndex(i, len(s))]); length > 0 { |
||||
for j, buf := 0, s[i:i+length]; j < len(buf); j++ { |
||||
result = append(result, toLowerASCII(buf[j])) |
||||
} |
||||
i += length - 1 |
||||
continue |
||||
} |
||||
} |
||||
} |
||||
result = append(result, toLowerASCII(c)) |
||||
} |
||||
return string(result) |
||||
} |
||||
|
||||
// AddCommonInitialism adds ss to list of common initialisms.
|
||||
func AddCommonInitialism(ss ...string) { |
||||
mu.Lock() |
||||
defer mu.Unlock() |
||||
for _, s := range ss { |
||||
commonInitialismMap[s] = struct{}{} |
||||
} |
||||
commonInitialisms = keys(commonInitialismMap) |
||||
commonInitialism = mustDoubleArray(newDoubleArray(commonInitialisms)) |
||||
longestLen = longestLength(commonInitialisms) |
||||
shortestLen = shortestLength(commonInitialisms, longestLen) |
||||
} |
||||
|
||||
// DelCommonInitialism deletes ss from list of common initialisms.
|
||||
func DelCommonInitialism(ss ...string) { |
||||
mu.Lock() |
||||
defer mu.Unlock() |
||||
for _, s := range ss { |
||||
delete(commonInitialismMap, s) |
||||
} |
||||
commonInitialisms = keys(commonInitialismMap) |
||||
commonInitialism = mustDoubleArray(newDoubleArray(commonInitialisms)) |
||||
longestLen = longestLength(commonInitialisms) |
||||
shortestLen = shortestLength(commonInitialisms, longestLen) |
||||
} |
||||
|
||||
func isUpperASCII(c byte) bool { |
||||
return 'A' <= c && c <= 'Z' |
||||
} |
||||
|
||||
func isLowerASCII(c byte) bool { |
||||
return 'a' <= c && c <= 'z' |
||||
} |
||||
|
||||
func toUpperASCII(c byte) byte { |
||||
if isLowerASCII(c) { |
||||
return c - ('a' - 'A') |
||||
} |
||||
return c |
||||
} |
||||
|
||||
func toLowerASCII(c byte) byte { |
||||
if isUpperASCII(c) { |
||||
return c + 'a' - 'A' |
||||
} |
||||
return c |
||||
} |
||||
|
||||
func nextIndex(i, maxlen int) int { |
||||
if n := i + longestLen; n < maxlen { |
||||
return n |
||||
} |
||||
return maxlen |
||||
} |
||||
|
||||
func keys(m map[string]struct{}) []string { |
||||
result := make([]string, 0, len(m)) |
||||
for k := range m { |
||||
result = append(result, k) |
||||
} |
||||
return result |
||||
} |
||||
|
||||
func shortestLength(strs []string, shortest int) int { |
||||
for _, s := range strs { |
||||
if candidate := utf8.RuneCountInString(s); candidate < shortest { |
||||
shortest = candidate |
||||
} |
||||
} |
||||
return shortest |
||||
} |
||||
|
||||
func longestLength(strs []string) (longest int) { |
||||
for _, s := range strs { |
||||
if candidate := utf8.RuneCountInString(s); candidate > longest { |
||||
longest = candidate |
||||
} |
||||
} |
||||
return longest |
||||
} |
@ -0,0 +1,19 @@ |
||||
Copyright (c) 2014 Naoya Inada <naoina@kuune.org> |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in |
||||
all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
THE SOFTWARE. |
@ -0,0 +1,392 @@ |
||||
# TOML parser and encoder library for Golang [![Build Status](https://travis-ci.org/naoina/toml.png?branch=master)](https://travis-ci.org/naoina/toml) |
||||
|
||||
[TOML](https://github.com/toml-lang/toml) parser and encoder library for [Golang](http://golang.org/). |
||||
|
||||
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). |
||||
|
||||
## Installation |
||||
|
||||
go get -u github.com/naoina/toml |
||||
|
||||
## Usage |
||||
|
||||
The following TOML save as `example.toml`. |
||||
|
||||
```toml |
||||
# This is a TOML document. Boom. |
||||
|
||||
title = "TOML Example" |
||||
|
||||
[owner] |
||||
name = "Lance Uppercut" |
||||
dob = 1979-05-27T07:32:00-08:00 # First class dates? Why not? |
||||
|
||||
[database] |
||||
server = "192.168.1.1" |
||||
ports = [ 8001, 8001, 8002 ] |
||||
connection_max = 5000 |
||||
enabled = true |
||||
|
||||
[servers] |
||||
|
||||
# You can indent as you please. Tabs or spaces. TOML don't care. |
||||
[servers.alpha] |
||||
ip = "10.0.0.1" |
||||
dc = "eqdc10" |
||||
|
||||
[servers.beta] |
||||
ip = "10.0.0.2" |
||||
dc = "eqdc10" |
||||
|
||||
[clients] |
||||
data = [ ["gamma", "delta"], [1, 2] ] |
||||
|
||||
# Line breaks are OK when inside arrays |
||||
hosts = [ |
||||
"alpha", |
||||
"omega" |
||||
] |
||||
``` |
||||
|
||||
Then above TOML will mapping to `tomlConfig` struct using `toml.Unmarshal`. |
||||
|
||||
```go |
||||
package main |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"os" |
||||
"time" |
||||
|
||||
"github.com/naoina/toml" |
||||
) |
||||
|
||||
type tomlConfig struct { |
||||
Title string |
||||
Owner struct { |
||||
Name string |
||||
Dob time.Time |
||||
} |
||||
Database struct { |
||||
Server string |
||||
Ports []int |
||||
ConnectionMax uint |
||||
Enabled bool |
||||
} |
||||
Servers map[string]ServerInfo |
||||
Clients struct { |
||||
Data [][]interface{} |
||||
Hosts []string |
||||
} |
||||
} |
||||
|
||||
type ServerInfo struct { |
||||
IP net.IP |
||||
DC string |
||||
} |
||||
|
||||
func main() { |
||||
f, err := os.Open("example.toml") |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer f.Close() |
||||
var config Config |
||||
if err := toml.NewDecoder(f).Decode(&config); err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
// then to use the unmarshaled config... |
||||
fmt.Println("IP of server 'alpha':", config.Servers["alpha"].IP) |
||||
} |
||||
``` |
||||
|
||||
## Mappings |
||||
|
||||
A key and value of TOML will map to the corresponding field. |
||||
The fields of struct for mapping must be exported. |
||||
|
||||
The rules of the mapping of key are following: |
||||
|
||||
#### Exact matching |
||||
|
||||
```toml |
||||
timeout_seconds = 256 |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
Timeout_seconds int |
||||
} |
||||
``` |
||||
|
||||
#### Camelcase matching |
||||
|
||||
```toml |
||||
server_name = "srv1" |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
ServerName string |
||||
} |
||||
``` |
||||
|
||||
#### Uppercase matching |
||||
|
||||
```toml |
||||
ip = "10.0.0.1" |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
IP string |
||||
} |
||||
``` |
||||
|
||||
See the following examples for the value mappings. |
||||
|
||||
### String |
||||
|
||||
```toml |
||||
val = "string" |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
Val string |
||||
} |
||||
``` |
||||
|
||||
### Integer |
||||
|
||||
```toml |
||||
val = 100 |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
Val int |
||||
} |
||||
``` |
||||
|
||||
All types that can be used are following: |
||||
|
||||
* int8 (from `-128` to `127`) |
||||
* int16 (from `-32768` to `32767`) |
||||
* int32 (from `-2147483648` to `2147483647`) |
||||
* int64 (from `-9223372036854775808` to `9223372036854775807`) |
||||
* int (same as `int32` on 32bit environment, or `int64` on 64bit environment) |
||||
* uint8 (from `0` to `255`) |
||||
* uint16 (from `0` to `65535`) |
||||
* uint32 (from `0` to `4294967295`) |
||||
* uint64 (from `0` to `18446744073709551615`) |
||||
* uint (same as `uint` on 32bit environment, or `uint64` on 64bit environment) |
||||
|
||||
### Float |
||||
|
||||
```toml |
||||
val = 3.1415 |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
Val float32 |
||||
} |
||||
``` |
||||
|
||||
All types that can be used are following: |
||||
|
||||
* float32 |
||||
* float64 |
||||
|
||||
### Boolean |
||||
|
||||
```toml |
||||
val = true |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
Val bool |
||||
} |
||||
``` |
||||
|
||||
### Datetime |
||||
|
||||
```toml |
||||
val = 2014-09-28T21:27:39Z |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
Val time.Time |
||||
} |
||||
``` |
||||
|
||||
### Array |
||||
|
||||
```toml |
||||
val = ["a", "b", "c"] |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
Val []string |
||||
} |
||||
``` |
||||
|
||||
Also following examples all can be mapped: |
||||
|
||||
```toml |
||||
val1 = [1, 2, 3] |
||||
val2 = [["a", "b"], ["c", "d"]] |
||||
val3 = [[1, 2, 3], ["a", "b", "c"]] |
||||
val4 = [[1, 2, 3], [["a", "b"], [true, false]]] |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
Val1 []int |
||||
Val2 [][]string |
||||
Val3 [][]interface{} |
||||
Val4 [][]interface{} |
||||
} |
||||
``` |
||||
|
||||
### Table |
||||
|
||||
```toml |
||||
[server] |
||||
type = "app" |
||||
|
||||
[server.development] |
||||
ip = "10.0.0.1" |
||||
|
||||
[server.production] |
||||
ip = "10.0.0.2" |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
Server map[string]Server |
||||
} |
||||
|
||||
type Server struct { |
||||
IP string |
||||
} |
||||
``` |
||||
|
||||
You can also use the following struct instead of map of struct. |
||||
|
||||
```go |
||||
type Config struct { |
||||
Server struct { |
||||
Development Server |
||||
Production Server |
||||
} |
||||
} |
||||
|
||||
type Server struct { |
||||
IP string |
||||
} |
||||
``` |
||||
|
||||
### Array of Tables |
||||
|
||||
```toml |
||||
[[fruit]] |
||||
name = "apple" |
||||
|
||||
[fruit.physical] |
||||
color = "red" |
||||
shape = "round" |
||||
|
||||
[[fruit.variety]] |
||||
name = "red delicious" |
||||
|
||||
[[fruit.variety]] |
||||
name = "granny smith" |
||||
|
||||
[[fruit]] |
||||
name = "banana" |
||||
|
||||
[[fruit.variety]] |
||||
name = "plantain" |
||||
``` |
||||
|
||||
```go |
||||
type Config struct { |
||||
Fruit []struct { |
||||
Name string |
||||
Physical struct { |
||||
Color string |
||||
Shape string |
||||
} |
||||
Variety []struct { |
||||
Name string |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Using the `encoding.TextUnmarshaler` interface |
||||
|
||||
Package toml supports `encoding.TextUnmarshaler` (and `encoding.TextMarshaler`). You can |
||||
use it to apply custom marshaling rules for certain types. The `UnmarshalText` method is |
||||
called with the value text found in the TOML input. TOML strings are passed unquoted. |
||||
|
||||
```toml |
||||
duration = "10s" |
||||
``` |
||||
|
||||
```go |
||||
import time |
||||
|
||||
type Duration time.Duration |
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler |
||||
func (d *Duration) UnmarshalText(data []byte) error { |
||||
duration, err := time.ParseDuration(string(data)) |
||||
if err == nil { |
||||
*d = Duration(duration) |
||||
} |
||||
return err |
||||
} |
||||
|
||||
// MarshalText implements encoding.TextMarshaler |
||||
func (d Duration) MarshalText() ([]byte, error) { |
||||
return []byte(time.Duration(d).String()), nil |
||||
} |
||||
|
||||
type ConfigWithDuration struct { |
||||
Duration Duration |
||||
} |
||||
``` |
||||
### Using the `toml.UnmarshalerRec` interface |
||||
|
||||
You can also override marshaling rules specifically for TOML using the `UnmarshalerRec` |
||||
and `MarshalerRec` interfaces. These are useful if you want to control how structs or |
||||
arrays are handled. You can apply additional validation or set unexported struct fields. |
||||
|
||||
Note: `encoding.TextUnmarshaler` and `encoding.TextMarshaler` should be preferred for |
||||
simple (scalar) values because they're also compatible with other formats like JSON or |
||||
YAML. |
||||
|
||||
[See the UnmarshalerRec example](https://godoc.org/github.com/naoina/toml/#example_UnmarshalerRec). |
||||
|
||||
### Using the `toml.Unmarshaler` interface |
||||
|
||||
If you want to deal with raw TOML syntax, use the `Unmarshaler` and `Marshaler` |
||||
interfaces. Their input and output is raw TOML syntax. As such, these interfaces are |
||||
useful if you want to handle TOML at the syntax level. |
||||
|
||||
[See the Unmarshaler example](https://godoc.org/github.com/naoina/toml/#example_Unmarshaler). |
||||
|
||||
## API documentation |
||||
|
||||
See [Godoc](http://godoc.org/github.com/naoina/toml). |
||||
|
||||
## License |
||||
|
||||
MIT |
@ -0,0 +1,192 @@ |
||||
package ast |
||||
|
||||
import ( |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
type Position struct { |
||||
Begin int |
||||
End int |
||||
} |
||||
|
||||
type Value interface { |
||||
Pos() int |
||||
End() int |
||||
Source() string |
||||
} |
||||
|
||||
type String struct { |
||||
Position Position |
||||
Value string |
||||
Data []rune |
||||
} |
||||
|
||||
func (s *String) Pos() int { |
||||
return s.Position.Begin |
||||
} |
||||
|
||||
func (s *String) End() int { |
||||
return s.Position.End |
||||
} |
||||
|
||||
func (s *String) Source() string { |
||||
return string(s.Data) |
||||
} |
||||
|
||||
type Integer struct { |
||||
Position Position |
||||
Value string |
||||
Data []rune |
||||
} |
||||
|
||||
func (i *Integer) Pos() int { |
||||
return i.Position.Begin |
||||
} |
||||
|
||||
func (i *Integer) End() int { |
||||
return i.Position.End |
||||
} |
||||
|
||||
func (i *Integer) Source() string { |
||||
return string(i.Data) |
||||
} |
||||
|
||||
func (i *Integer) Int() (int64, error) { |
||||
return strconv.ParseInt(i.Value, 10, 64) |
||||
} |
||||
|
||||
type Float struct { |
||||
Position Position |
||||
Value string |
||||
Data []rune |
||||
} |
||||
|
||||
func (f *Float) Pos() int { |
||||
return f.Position.Begin |
||||
} |
||||
|
||||
func (f *Float) End() int { |
||||
return f.Position.End |
||||
} |
||||
|
||||
func (f *Float) Source() string { |
||||
return string(f.Data) |
||||
} |
||||
|
||||
func (f *Float) Float() (float64, error) { |
||||
return strconv.ParseFloat(f.Value, 64) |
||||
} |
||||
|
||||
type Boolean struct { |
||||
Position Position |
||||
Value string |
||||
Data []rune |
||||
} |
||||
|
||||
func (b *Boolean) Pos() int { |
||||
return b.Position.Begin |
||||
} |
||||
|
||||
func (b *Boolean) End() int { |
||||
return b.Position.End |
||||
} |
||||
|
||||
func (b *Boolean) Source() string { |
||||
return string(b.Data) |
||||
} |
||||
|
||||
func (b *Boolean) Boolean() (bool, error) { |
||||
return strconv.ParseBool(b.Value) |
||||
} |
||||
|
||||
type Datetime struct { |
||||
Position Position |
||||
Value string |
||||
Data []rune |
||||
} |
||||
|
||||
func (d *Datetime) Pos() int { |
||||
return d.Position.Begin |
||||
} |
||||
|
||||
func (d *Datetime) End() int { |
||||
return d.Position.End |
||||
} |
||||
|
||||
func (d *Datetime) Source() string { |
||||
return string(d.Data) |
||||
} |
||||
|
||||
func (d *Datetime) Time() (time.Time, error) { |
||||
switch { |
||||
case !strings.Contains(d.Value, ":"): |
||||
return time.Parse("2006-01-02", d.Value) |
||||
case !strings.Contains(d.Value, "-"): |
||||
return time.Parse("15:04:05.999999999", d.Value) |
||||
default: |
||||
return time.Parse(time.RFC3339Nano, d.Value) |
||||
} |
||||
} |
||||
|
||||
type Array struct { |
||||
Position Position |
||||
Value []Value |
||||
Data []rune |
||||
} |
||||
|
||||
func (a *Array) Pos() int { |
||||
return a.Position.Begin |
||||
} |
||||
|
||||
func (a *Array) End() int { |
||||
return a.Position.End |
||||
} |
||||
|
||||
func (a *Array) Source() string { |
||||
return string(a.Data) |
||||
} |
||||
|
||||
type TableType uint8 |
||||
|
||||
const ( |
||||
TableTypeNormal TableType = iota |
||||
TableTypeArray |
||||
) |
||||
|
||||
var tableTypes = [...]string{ |
||||
"normal", |
||||
"array", |
||||
} |
||||
|
||||
func (t TableType) String() string { |
||||
return tableTypes[t] |
||||
} |
||||
|
||||
type Table struct { |
||||
Position Position |
||||
Line int |
||||
Name string |
||||
Fields map[string]interface{} |
||||
Type TableType |
||||
Data []rune |
||||
} |
||||
|
||||
func (t *Table) Pos() int { |
||||
return t.Position.Begin |
||||
} |
||||
|
||||
func (t *Table) End() int { |
||||
return t.Position.End |
||||
} |
||||
|
||||
func (t *Table) Source() string { |
||||
return string(t.Data) |
||||
} |
||||
|
||||
type KeyValue struct { |
||||
Key string |
||||
Value Value |
||||
Line int |
||||
} |
@ -0,0 +1,86 @@ |
||||
package toml |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io" |
||||
"reflect" |
||||
"strings" |
||||
|
||||
stringutil "github.com/naoina/go-stringutil" |
||||
"github.com/naoina/toml/ast" |
||||
) |
||||
|
||||
// Config contains options for encoding and decoding.
|
||||
type Config struct { |
||||
// NormFieldName is used to match TOML keys to struct fields. The function runs for
|
||||
// both input keys and struct field names and should return a string that makes the
|
||||
// two match. You must set this field to use the decoder.
|
||||
//
|
||||
// Example: The function in the default config removes _ and lowercases all keys. This
|
||||
// allows a key called 'api_key' to match the struct field 'APIKey' because both are
|
||||
// normalized to 'apikey'.
|
||||
//
|
||||
// Note that NormFieldName is not used for fields which define a TOML
|
||||
// key through the struct tag.
|
||||
NormFieldName func(typ reflect.Type, keyOrField string) string |
||||
|
||||
// FieldToKey determines the TOML key of a struct field when encoding.
|
||||
// You must set this field to use the encoder.
|
||||
//
|
||||
// Note that FieldToKey is not used for fields which define a TOML
|
||||
// key through the struct tag.
|
||||
FieldToKey func(typ reflect.Type, field string) string |
||||
|
||||
// MissingField, if non-nil, is called when the decoder encounters a key for which no
|
||||
// matching struct field exists. The default behavior is to return an error.
|
||||
MissingField func(typ reflect.Type, key string) error |
||||
} |
||||
|
||||
// DefaultConfig contains the default options for encoding and decoding.
|
||||
// Snake case (i.e. 'foo_bar') is used for key names.
|
||||
var DefaultConfig = Config{ |
||||
NormFieldName: defaultNormFieldName, |
||||
FieldToKey: snakeCase, |
||||
} |
||||
|
||||
func defaultNormFieldName(typ reflect.Type, s string) string { |
||||
return strings.Replace(strings.ToLower(s), "_", "", -1) |
||||
} |
||||
|
||||
func snakeCase(typ reflect.Type, s string) string { |
||||
return stringutil.ToSnakeCase(s) |
||||
} |
||||
|
||||
func defaultMissingField(typ reflect.Type, key string) error { |
||||
return fmt.Errorf("field corresponding to `%s' is not defined in %v", key, typ) |
||||
} |
||||
|
||||
// NewEncoder returns a new Encoder that writes to w.
|
||||
// It is shorthand for DefaultConfig.NewEncoder(w).
|
||||
func NewEncoder(w io.Writer) *Encoder { |
||||
return DefaultConfig.NewEncoder(w) |
||||
} |
||||
|
||||
// Marshal returns the TOML encoding of v.
|
||||
// It is shorthand for DefaultConfig.Marshal(v).
|
||||
func Marshal(v interface{}) ([]byte, error) { |
||||
return DefaultConfig.Marshal(v) |
||||
} |
||||
|
||||
// Unmarshal parses the TOML data and stores the result in the value pointed to by v.
|
||||
// It is shorthand for DefaultConfig.Unmarshal(data, v).
|
||||
func Unmarshal(data []byte, v interface{}) error { |
||||
return DefaultConfig.Unmarshal(data, v) |
||||
} |
||||
|
||||
// UnmarshalTable applies the contents of an ast.Table to the value pointed at by v.
|
||||
// It is shorthand for DefaultConfig.UnmarshalTable(t, v).
|
||||
func UnmarshalTable(t *ast.Table, v interface{}) error { |
||||
return DefaultConfig.UnmarshalTable(t, v) |
||||
} |
||||
|
||||
// NewDecoder returns a new Decoder that reads from r.
|
||||
// It is shorthand for DefaultConfig.NewDecoder(r).
|
||||
func NewDecoder(r io.Reader) *Decoder { |
||||
return DefaultConfig.NewDecoder(r) |
||||
} |
@ -0,0 +1,478 @@ |
||||
// Package toml encodes and decodes the TOML configuration format using reflection.
|
||||
//
|
||||
// This library is compatible with TOML version v0.4.0.
|
||||
package toml |
||||
|
||||
import ( |
||||
"encoding" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"reflect" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/naoina/toml/ast" |
||||
) |
||||
|
||||
const ( |
||||
tableSeparator = '.' |
||||
) |
||||
|
||||
var ( |
||||
escapeReplacer = strings.NewReplacer( |
||||
"\b", "\\n", |
||||
"\f", "\\f", |
||||
"\n", "\\n", |
||||
"\r", "\\r", |
||||
"\t", "\\t", |
||||
) |
||||
underscoreReplacer = strings.NewReplacer( |
||||
"_", "", |
||||
) |
||||
) |
||||
|
||||
var timeType = reflect.TypeOf(time.Time{}) |
||||
|
||||
// Unmarshal parses the TOML data and stores the result in the value pointed to by v.
|
||||
//
|
||||
// Unmarshal will mapped to v that according to following rules:
|
||||
//
|
||||
// TOML strings to string
|
||||
// TOML integers to any int type
|
||||
// TOML floats to float32 or float64
|
||||
// TOML booleans to bool
|
||||
// TOML datetimes to time.Time
|
||||
// TOML arrays to any type of slice
|
||||
// TOML tables to struct or map
|
||||
// TOML array tables to slice of struct or map
|
||||
func (cfg *Config) Unmarshal(data []byte, v interface{}) error { |
||||
table, err := Parse(data) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if err := cfg.UnmarshalTable(table, v); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// A Decoder reads and decodes TOML from an input stream.
|
||||
type Decoder struct { |
||||
r io.Reader |
||||
cfg *Config |
||||
} |
||||
|
||||
// NewDecoder returns a new Decoder that reads from r.
|
||||
// Note that it reads all from r before parsing it.
|
||||
func (cfg *Config) NewDecoder(r io.Reader) *Decoder { |
||||
return &Decoder{r, cfg} |
||||
} |
||||
|
||||
// Decode parses the TOML data from its input and stores it in the value pointed to by v.
|
||||
// See the documentation for Unmarshal for details about the conversion of TOML into a Go value.
|
||||
func (d *Decoder) Decode(v interface{}) error { |
||||
b, err := ioutil.ReadAll(d.r) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return d.cfg.Unmarshal(b, v) |
||||
} |
||||
|
||||
// UnmarshalerRec may be implemented by types to customize their behavior when being
|
||||
// unmarshaled from TOML. You can use it to implement custom validation or to set
|
||||
// unexported fields.
|
||||
//
|
||||
// UnmarshalTOML receives a function that can be called to unmarshal the original TOML
|
||||
// value into a field or variable. It is safe to call the function more than once if
|
||||
// necessary.
|
||||
type UnmarshalerRec interface { |
||||
UnmarshalTOML(fn func(interface{}) error) error |
||||
} |
||||
|
||||
// Unmarshaler can be used to capture and process raw TOML source of a table or value.
|
||||
// UnmarshalTOML must copy the input if it wishes to retain it after returning.
|
||||
//
|
||||
// Note: this interface is retained for backwards compatibility. You probably want
|
||||
// to implement encoding.TextUnmarshaler or UnmarshalerRec instead.
|
||||
type Unmarshaler interface { |
||||
UnmarshalTOML(input []byte) error |
||||
} |
||||
|
||||
// UnmarshalTable applies the contents of an ast.Table to the value pointed at by v.
|
||||
//
|
||||
// UnmarshalTable will mapped to v that according to following rules:
|
||||
//
|
||||
// TOML strings to string
|
||||
// TOML integers to any int type
|
||||
// TOML floats to float32 or float64
|
||||
// TOML booleans to bool
|
||||
// TOML datetimes to time.Time
|
||||
// TOML arrays to any type of slice
|
||||
// TOML tables to struct or map
|
||||
// TOML array tables to slice of struct or map
|
||||
func (cfg *Config) UnmarshalTable(t *ast.Table, v interface{}) error { |
||||
rv := reflect.ValueOf(v) |
||||
toplevelMap := rv.Kind() == reflect.Map |
||||
if (!toplevelMap && rv.Kind() != reflect.Ptr) || rv.IsNil() { |
||||
return &invalidUnmarshalError{reflect.TypeOf(v)} |
||||
} |
||||
return unmarshalTable(cfg, rv, t, toplevelMap) |
||||
} |
||||
|
||||
// used for UnmarshalerRec.
|
||||
func unmarshalTableOrValue(cfg *Config, rv reflect.Value, av interface{}) error { |
||||
if (rv.Kind() != reflect.Ptr && rv.Kind() != reflect.Map) || rv.IsNil() { |
||||
return &invalidUnmarshalError{rv.Type()} |
||||
} |
||||
rv = indirect(rv) |
||||
|
||||
switch av.(type) { |
||||
case *ast.KeyValue, *ast.Table, []*ast.Table: |
||||
if err := unmarshalField(cfg, rv, av); err != nil { |
||||
return lineError(fieldLineNumber(av), err) |
||||
} |
||||
return nil |
||||
case ast.Value: |
||||
return setValue(cfg, rv, av.(ast.Value)) |
||||
default: |
||||
panic(fmt.Sprintf("BUG: unhandled AST node type %T", av)) |
||||
} |
||||
} |
||||
|
||||
// unmarshalTable unmarshals the fields of a table into a struct or map.
|
||||
//
|
||||
// toplevelMap is true when rv is an (unadressable) map given to UnmarshalTable. In this
|
||||
// (special) case, the map is used as-is instead of creating a new map.
|
||||
func unmarshalTable(cfg *Config, rv reflect.Value, t *ast.Table, toplevelMap bool) error { |
||||
rv = indirect(rv) |
||||
if err, ok := setUnmarshaler(cfg, rv, t); ok { |
||||
return lineError(t.Line, err) |
||||
} |
||||
switch { |
||||
case rv.Kind() == reflect.Struct: |
||||
fc := makeFieldCache(cfg, rv.Type()) |
||||
for key, fieldAst := range t.Fields { |
||||
fv, fieldName, err := fc.findField(cfg, rv, key) |
||||
if err != nil { |
||||
return lineError(fieldLineNumber(fieldAst), err) |
||||
} |
||||
if fv.IsValid() { |
||||
if err := unmarshalField(cfg, fv, fieldAst); err != nil { |
||||
return lineErrorField(fieldLineNumber(fieldAst), rv.Type().String()+"."+fieldName, err) |
||||
} |
||||
} |
||||
} |
||||
case rv.Kind() == reflect.Map || isEface(rv): |
||||
m := rv |
||||
if !toplevelMap { |
||||
if rv.Kind() == reflect.Interface { |
||||
m = reflect.ValueOf(make(map[string]interface{})) |
||||
} else { |
||||
m = reflect.MakeMap(rv.Type()) |
||||
} |
||||
} |
||||
elemtyp := m.Type().Elem() |
||||
for key, fieldAst := range t.Fields { |
||||
kv, err := unmarshalMapKey(m.Type().Key(), key) |
||||
if err != nil { |
||||
return lineError(fieldLineNumber(fieldAst), err) |
||||
} |
||||
fv := reflect.New(elemtyp).Elem() |
||||
if err := unmarshalField(cfg, fv, fieldAst); err != nil { |
||||
return lineError(fieldLineNumber(fieldAst), err) |
||||
} |
||||
m.SetMapIndex(kv, fv) |
||||
} |
||||
if !toplevelMap { |
||||
rv.Set(m) |
||||
} |
||||
default: |
||||
return lineError(t.Line, &unmarshalTypeError{"table", "struct or map", rv.Type()}) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func fieldLineNumber(fieldAst interface{}) int { |
||||
switch av := fieldAst.(type) { |
||||
case *ast.KeyValue: |
||||
return av.Line |
||||
case *ast.Table: |
||||
return av.Line |
||||
case []*ast.Table: |
||||
return av[0].Line |
||||
default: |
||||
panic(fmt.Sprintf("BUG: unhandled node type %T", fieldAst)) |
||||
} |
||||
} |
||||
|
||||
func unmarshalField(cfg *Config, rv reflect.Value, fieldAst interface{}) error { |
||||
switch av := fieldAst.(type) { |
||||
case *ast.KeyValue: |
||||
return setValue(cfg, rv, av.Value) |
||||
case *ast.Table: |
||||
return unmarshalTable(cfg, rv, av, false) |
||||
case []*ast.Table: |
||||
rv = indirect(rv) |
||||
if err, ok := setUnmarshaler(cfg, rv, fieldAst); ok { |
||||
return err |
||||
} |
||||
var slice reflect.Value |
||||
switch { |
||||
case rv.Kind() == reflect.Slice: |
||||
slice = reflect.MakeSlice(rv.Type(), len(av), len(av)) |
||||
case isEface(rv): |
||||
slice = reflect.ValueOf(make([]interface{}, len(av))) |
||||
default: |
||||
return &unmarshalTypeError{"array table", "slice", rv.Type()} |
||||
} |
||||
for i, tbl := range av { |
||||
vv := reflect.New(slice.Type().Elem()).Elem() |
||||
if err := unmarshalTable(cfg, vv, tbl, false); err != nil { |
||||
return err |
||||
} |
||||
slice.Index(i).Set(vv) |
||||
} |
||||
rv.Set(slice) |
||||
default: |
||||
panic(fmt.Sprintf("BUG: unhandled AST node type %T", av)) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func unmarshalMapKey(typ reflect.Type, key string) (reflect.Value, error) { |
||||
rv := reflect.New(typ).Elem() |
||||
if u, ok := rv.Addr().Interface().(encoding.TextUnmarshaler); ok { |
||||
return rv, u.UnmarshalText([]byte(key)) |
||||
} |
||||
switch typ.Kind() { |
||||
case reflect.String: |
||||
rv.SetString(key) |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
i, err := strconv.ParseInt(key, 10, int(typ.Size()*8)) |
||||
if err != nil { |
||||
return rv, convertNumError(typ.Kind(), err) |
||||
} |
||||
rv.SetInt(i) |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
||||
i, err := strconv.ParseUint(key, 10, int(typ.Size()*8)) |
||||
if err != nil { |
||||
return rv, convertNumError(typ.Kind(), err) |
||||
} |
||||
rv.SetUint(i) |
||||
default: |
||||
return rv, fmt.Errorf("invalid map key type %s", typ) |
||||
} |
||||
return rv, nil |
||||
} |
||||
|
||||
func setValue(cfg *Config, lhs reflect.Value, val ast.Value) error { |
||||
lhs = indirect(lhs) |
||||
if err, ok := setUnmarshaler(cfg, lhs, val); ok { |
||||
return err |
||||
} |
||||
if err, ok := setTextUnmarshaler(lhs, val); ok { |
||||
return err |
||||
} |
||||
switch v := val.(type) { |
||||
case *ast.Integer: |
||||
return setInt(lhs, v) |
||||
case *ast.Float: |
||||
return setFloat(lhs, v) |
||||
case *ast.String: |
||||
return setString(lhs, v) |
||||
case *ast.Boolean: |
||||
return setBoolean(lhs, v) |
||||
case *ast.Datetime: |
||||
return setDatetime(lhs, v) |
||||
case *ast.Array: |
||||
return setArray(cfg, lhs, v) |
||||
default: |
||||
panic(fmt.Sprintf("BUG: unhandled node type %T", v)) |
||||
} |
||||
} |
||||
|
||||
func indirect(rv reflect.Value) reflect.Value { |
||||
for rv.Kind() == reflect.Ptr { |
||||
if rv.IsNil() { |
||||
rv.Set(reflect.New(rv.Type().Elem())) |
||||
} |
||||
rv = rv.Elem() |
||||
} |
||||
return rv |
||||
} |
||||
|
||||
func setUnmarshaler(cfg *Config, lhs reflect.Value, av interface{}) (error, bool) { |
||||
if lhs.CanAddr() { |
||||
if u, ok := lhs.Addr().Interface().(UnmarshalerRec); ok { |
||||
err := u.UnmarshalTOML(func(v interface{}) error { |
||||
return unmarshalTableOrValue(cfg, reflect.ValueOf(v), av) |
||||
}) |
||||
return err, true |
||||
} |
||||
if u, ok := lhs.Addr().Interface().(Unmarshaler); ok { |
||||
return u.UnmarshalTOML(unmarshalerSource(av)), true |
||||
} |
||||
} |
||||
return nil, false |
||||
} |
||||
|
||||
func unmarshalerSource(av interface{}) []byte { |
||||
var source []byte |
||||
switch av := av.(type) { |
||||
case []*ast.Table: |
||||
for i, tab := range av { |
||||
source = append(source, tab.Source()...) |
||||
if i != len(av)-1 { |
||||
source = append(source, '\n') |
||||
} |
||||
} |
||||
case ast.Value: |
||||
source = []byte(av.Source()) |
||||
default: |
||||
panic(fmt.Sprintf("BUG: unhandled node type %T", av)) |
||||
} |
||||
return source |
||||
} |
||||
|
||||
func setTextUnmarshaler(lhs reflect.Value, val ast.Value) (error, bool) { |
||||
if !lhs.CanAddr() { |
||||
return nil, false |
||||
} |
||||
u, ok := lhs.Addr().Interface().(encoding.TextUnmarshaler) |
||||
if !ok || lhs.Type() == timeType { |
||||
return nil, false |
||||
} |
||||
var data string |
||||
switch val := val.(type) { |
||||
case *ast.Array: |
||||
return &unmarshalTypeError{"array", "", lhs.Type()}, true |
||||
case *ast.String: |
||||
data = val.Value |
||||
default: |
||||
data = val.Source() |
||||
} |
||||
return u.UnmarshalText([]byte(data)), true |
||||
} |
||||
|
||||
func setInt(fv reflect.Value, v *ast.Integer) error { |
||||
k := fv.Kind() |
||||
switch { |
||||
case k >= reflect.Int && k <= reflect.Int64: |
||||
i, err := strconv.ParseInt(v.Value, 10, int(fv.Type().Size()*8)) |
||||
if err != nil { |
||||
return convertNumError(fv.Kind(), err) |
||||
} |
||||
fv.SetInt(i) |
||||
case k >= reflect.Uint && k <= reflect.Uintptr: |
||||
i, err := strconv.ParseUint(v.Value, 10, int(fv.Type().Size()*8)) |
||||
if err != nil { |
||||
return convertNumError(fv.Kind(), err) |
||||
} |
||||
fv.SetUint(i) |
||||
case isEface(fv): |
||||
i, err := strconv.ParseInt(v.Value, 10, 64) |
||||
if err != nil { |
||||
return convertNumError(reflect.Int64, err) |
||||
} |
||||
fv.Set(reflect.ValueOf(i)) |
||||
default: |
||||
return &unmarshalTypeError{"integer", "", fv.Type()} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func setFloat(fv reflect.Value, v *ast.Float) error { |
||||
f, err := v.Float() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
switch { |
||||
case fv.Kind() == reflect.Float32 || fv.Kind() == reflect.Float64: |
||||
if fv.OverflowFloat(f) { |
||||
return &overflowError{fv.Kind(), v.Value} |
||||
} |
||||
fv.SetFloat(f) |
||||
case isEface(fv): |
||||
fv.Set(reflect.ValueOf(f)) |
||||
default: |
||||
return &unmarshalTypeError{"float", "", fv.Type()} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func setString(fv reflect.Value, v *ast.String) error { |
||||
switch { |
||||
case fv.Kind() == reflect.String: |
||||
fv.SetString(v.Value) |
||||
case isEface(fv): |
||||
fv.Set(reflect.ValueOf(v.Value)) |
||||
default: |
||||
return &unmarshalTypeError{"string", "", fv.Type()} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func setBoolean(fv reflect.Value, v *ast.Boolean) error { |
||||
b, _ := v.Boolean() |
||||
switch { |
||||
case fv.Kind() == reflect.Bool: |
||||
fv.SetBool(b) |
||||
case isEface(fv): |
||||
fv.Set(reflect.ValueOf(b)) |
||||
default: |
||||
return &unmarshalTypeError{"boolean", "", fv.Type()} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func setDatetime(rv reflect.Value, v *ast.Datetime) error { |
||||
t, err := v.Time() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !timeType.AssignableTo(rv.Type()) { |
||||
return &unmarshalTypeError{"datetime", "", rv.Type()} |
||||
} |
||||
rv.Set(reflect.ValueOf(t)) |
||||
return nil |
||||
} |
||||
|
||||
func setArray(cfg *Config, rv reflect.Value, v *ast.Array) error { |
||||
var slicetyp reflect.Type |
||||
switch { |
||||
case rv.Kind() == reflect.Slice: |
||||
slicetyp = rv.Type() |
||||
case isEface(rv): |
||||
slicetyp = reflect.SliceOf(rv.Type()) |
||||
default: |
||||
return &unmarshalTypeError{"array", "slice", rv.Type()} |
||||
} |
||||
|
||||
if len(v.Value) == 0 { |
||||
// Ensure defined slices are always set to a non-nil value.
|
||||
rv.Set(reflect.MakeSlice(slicetyp, 0, 0)) |
||||
return nil |
||||
} |
||||
|
||||
tomltyp := reflect.TypeOf(v.Value[0]) |
||||
slice := reflect.MakeSlice(slicetyp, len(v.Value), len(v.Value)) |
||||
typ := slicetyp.Elem() |
||||
for i, vv := range v.Value { |
||||
if i > 0 && tomltyp != reflect.TypeOf(vv) { |
||||
return errArrayMultiType |
||||
} |
||||
tmp := reflect.New(typ).Elem() |
||||
if err := setValue(cfg, tmp, vv); err != nil { |
||||
return err |
||||
} |
||||
slice.Index(i).Set(tmp) |
||||
} |
||||
rv.Set(slice) |
||||
return nil |
||||
} |
||||
|
||||
func isEface(rv reflect.Value) bool { |
||||
return rv.Kind() == reflect.Interface && rv.Type().NumMethod() == 0 |
||||
} |
@ -0,0 +1,398 @@ |
||||
package toml |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding" |
||||
"fmt" |
||||
"io" |
||||
"reflect" |
||||
"sort" |
||||
"strconv" |
||||
"time" |
||||
|
||||
"github.com/naoina/toml/ast" |
||||
) |
||||
|
||||
const ( |
||||
tagOmitempty = "omitempty" |
||||
tagSkip = "-" |
||||
) |
||||
|
||||
// Marshal returns the TOML encoding of v.
|
||||
//
|
||||
// Struct values encode as TOML. Each exported struct field becomes a field of
|
||||
// the TOML structure unless
|
||||
// - the field's tag is "-", or
|
||||
// - the field is empty and its tag specifies the "omitempty" option.
|
||||
//
|
||||
// The "toml" key in the struct field's tag value is the key name, followed by
|
||||
// an optional comma and options. Examples:
|
||||
//
|
||||
// // Field is ignored by this package.
|
||||
// Field int `toml:"-"`
|
||||
//
|
||||
// // Field appears in TOML as key "myName".
|
||||
// Field int `toml:"myName"`
|
||||
//
|
||||
// // Field appears in TOML as key "myName" and the field is omitted from the
|
||||
// // result of encoding if its value is empty.
|
||||
// Field int `toml:"myName,omitempty"`
|
||||
//
|
||||
// // Field appears in TOML as key "field", but the field is skipped if
|
||||
// // empty. Note the leading comma.
|
||||
// Field int `toml:",omitempty"`
|
||||
func (cfg *Config) Marshal(v interface{}) ([]byte, error) { |
||||
buf := new(bytes.Buffer) |
||||
err := cfg.NewEncoder(buf).Encode(v) |
||||
return buf.Bytes(), err |
||||
} |
||||
|
||||
// A Encoder writes TOML to an output stream.
|
||||
type Encoder struct { |
||||
w io.Writer |
||||
cfg *Config |
||||
} |
||||
|
||||
// NewEncoder returns a new Encoder that writes to w.
|
||||
func (cfg *Config) NewEncoder(w io.Writer) *Encoder { |
||||
return &Encoder{w, cfg} |
||||
} |
||||
|
||||
// Encode writes the TOML of v to the stream.
|
||||
// See the documentation for Marshal for details about the conversion of Go values to TOML.
|
||||
func (e *Encoder) Encode(v interface{}) error { |
||||
rv := reflect.ValueOf(v) |
||||
for rv.Kind() == reflect.Ptr { |
||||
if rv.IsNil() { |
||||
return &marshalNilError{rv.Type()} |
||||
} |
||||
rv = rv.Elem() |
||||
} |
||||
buf := &tableBuf{typ: ast.TableTypeNormal} |
||||
var err error |
||||
switch rv.Kind() { |
||||
case reflect.Struct: |
||||
err = buf.structFields(e.cfg, rv) |
||||
case reflect.Map: |
||||
err = buf.mapFields(e.cfg, rv) |
||||
default: |
||||
err = &marshalTableError{rv.Type()} |
||||
} |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return buf.writeTo(e.w, "") |
||||
} |
||||
|
||||
// Marshaler can be implemented to override the encoding of TOML values. The returned text
|
||||
// must be a simple TOML value (i.e. not a table) and is inserted into marshaler output.
|
||||
//
|
||||
// This interface exists for backwards-compatibility reasons. You probably want to
|
||||
// implement encoding.TextMarshaler or MarshalerRec instead.
|
||||
type Marshaler interface { |
||||
MarshalTOML() ([]byte, error) |
||||
} |
||||
|
||||
// MarshalerRec can be implemented to override the TOML encoding of a type.
|
||||
// The returned value is marshaled in place of the receiver.
|
||||
type MarshalerRec interface { |
||||
MarshalTOML() (interface{}, error) |
||||
} |
||||
|
||||
type tableBuf struct { |
||||
name string // already escaped / quoted
|
||||
body []byte |
||||
children []*tableBuf |
||||
typ ast.TableType |
||||
arrayDepth int |
||||
} |
||||
|
||||
func (b *tableBuf) writeTo(w io.Writer, prefix string) error { |
||||
key := b.name // TODO: escape dots
|
||||
if prefix != "" { |
||||
key = prefix + "." + key |
||||
} |
||||
|
||||
if b.name != "" { |
||||
head := "[" + key + "]" |
||||
if b.typ == ast.TableTypeArray { |
||||
head = "[" + head + "]" |
||||
} |
||||
head += "\n" |
||||
if _, err := io.WriteString(w, head); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
if _, err := w.Write(b.body); err != nil { |
||||
return err |
||||
} |
||||
|
||||
for i, child := range b.children { |
||||
if len(b.body) > 0 || i > 0 { |
||||
if _, err := w.Write([]byte("\n")); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
if err := child.writeTo(w, key); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (b *tableBuf) newChild(name string) *tableBuf { |
||||
child := &tableBuf{name: quoteName(name), typ: ast.TableTypeNormal} |
||||
if b.arrayDepth > 0 { |
||||
child.typ = ast.TableTypeArray |
||||
} |
||||
return child |
||||
} |
||||
|
||||
func (b *tableBuf) addChild(child *tableBuf) { |
||||
// Empty table elision: we can avoid writing a table that doesn't have any keys on its
|
||||
// own. Array tables can't be elided because they define array elements (which would
|
||||
// be missing if elided).
|
||||
if len(child.body) == 0 && child.typ == ast.TableTypeNormal { |
||||
for _, gchild := range child.children { |
||||
gchild.name = child.name + "." + gchild.name |
||||
b.addChild(gchild) |
||||
} |
||||
return |
||||
} |
||||
b.children = append(b.children, child) |
||||
} |
||||
|
||||
func (b *tableBuf) structFields(cfg *Config, rv reflect.Value) error { |
||||
rt := rv.Type() |
||||
for i := 0; i < rv.NumField(); i++ { |
||||
ft := rt.Field(i) |
||||
if ft.PkgPath != "" && !ft.Anonymous { // not exported
|
||||
continue |
||||
} |
||||
name, rest := extractTag(ft.Tag.Get(fieldTagName)) |
||||
if name == tagSkip { |
||||
continue |
||||
} |
||||
fv := rv.Field(i) |
||||
if rest == tagOmitempty && isEmptyValue(fv) { |
||||
continue |
||||
} |
||||
if name == "" { |
||||
name = cfg.FieldToKey(rt, ft.Name) |
||||
} |
||||
if err := b.field(cfg, name, fv); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
type mapKeyList []struct { |
||||
key string |
||||
value reflect.Value |
||||
} |
||||
|
||||
func (l mapKeyList) Len() int { return len(l) } |
||||
func (l mapKeyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } |
||||
func (l mapKeyList) Less(i, j int) bool { return l[i].key < l[j].key } |
||||
|
||||
func (b *tableBuf) mapFields(cfg *Config, rv reflect.Value) error { |
||||
keys := rv.MapKeys() |
||||
keylist := make(mapKeyList, len(keys)) |
||||
for i, key := range keys { |
||||
var err error |
||||
keylist[i].key, err = encodeMapKey(key) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
keylist[i].value = rv.MapIndex(key) |
||||
} |
||||
sort.Sort(keylist) |
||||
|
||||
for _, kv := range keylist { |
||||
if err := b.field(cfg, kv.key, kv.value); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (b *tableBuf) field(cfg *Config, name string, rv reflect.Value) error { |
||||
off := len(b.body) |
||||
b.body = append(b.body, quoteName(name)...) |
||||
b.body = append(b.body, " = "...) |
||||
isTable, err := b.value(cfg, rv, name) |
||||
if isTable { |
||||
b.body = b.body[:off] // rub out "key ="
|
||||
} else { |
||||
b.body = append(b.body, '\n') |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func (b *tableBuf) value(cfg *Config, rv reflect.Value, name string) (bool, error) { |
||||
isMarshaler, isTable, err := b.marshaler(cfg, rv, name) |
||||
if isMarshaler { |
||||
return isTable, err |
||||
} |
||||
switch rv.Kind() { |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
b.body = strconv.AppendInt(b.body, rv.Int(), 10) |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||
b.body = strconv.AppendUint(b.body, rv.Uint(), 10) |
||||
case reflect.Float32, reflect.Float64: |
||||
b.body = strconv.AppendFloat(b.body, rv.Float(), 'e', -1, 64) |
||||
case reflect.Bool: |
||||
b.body = strconv.AppendBool(b.body, rv.Bool()) |
||||
case reflect.String: |
||||
b.body = strconv.AppendQuote(b.body, rv.String()) |
||||
case reflect.Ptr, reflect.Interface: |
||||
if rv.IsNil() { |
||||
return false, &marshalNilError{rv.Type()} |
||||
} |
||||
return b.value(cfg, rv.Elem(), name) |
||||
case reflect.Slice, reflect.Array: |
||||
rvlen := rv.Len() |
||||
if rvlen == 0 { |
||||
b.body = append(b.body, '[', ']') |
||||
return false, nil |
||||
} |
||||
|
||||
b.arrayDepth++ |
||||
wroteElem := false |
||||
b.body = append(b.body, '[') |
||||
for i := 0; i < rvlen; i++ { |
||||
isTable, err := b.value(cfg, rv.Index(i), name) |
||||
if err != nil { |
||||
return isTable, err |
||||
} |
||||
wroteElem = wroteElem || !isTable |
||||
if wroteElem { |
||||
if i < rvlen-1 { |
||||
b.body = append(b.body, ',', ' ') |
||||
} else { |
||||
b.body = append(b.body, ']') |
||||
} |
||||
} |
||||
} |
||||
if !wroteElem { |
||||
b.body = b.body[:len(b.body)-1] // rub out '['
|
||||
} |
||||
b.arrayDepth-- |
||||
return !wroteElem, nil |
||||
case reflect.Struct: |
||||
child := b.newChild(name) |
||||
err := child.structFields(cfg, rv) |
||||
b.addChild(child) |
||||
return true, err |
||||
case reflect.Map: |
||||
child := b.newChild(name) |
||||
err := child.mapFields(cfg, rv) |
||||
b.addChild(child) |
||||
return true, err |
||||
default: |
||||
return false, fmt.Errorf("toml: marshal: unsupported type %v", rv.Kind()) |
||||
} |
||||
return false, nil |
||||
} |
||||
|
||||
func (b *tableBuf) marshaler(cfg *Config, rv reflect.Value, name string) (handled, isTable bool, err error) { |
||||
switch t := rv.Interface().(type) { |
||||
case encoding.TextMarshaler: |
||||
enc, err := t.MarshalText() |
||||
if err != nil { |
||||
return true, false, err |
||||
} |
||||
b.body = encodeTextMarshaler(b.body, string(enc)) |
||||
return true, false, nil |
||||
case MarshalerRec: |
||||
newval, err := t.MarshalTOML() |
||||
if err != nil { |
||||
return true, false, err |
||||
} |
||||
isTable, err = b.value(cfg, reflect.ValueOf(newval), name) |
||||
return true, isTable, err |
||||
case Marshaler: |
||||
enc, err := t.MarshalTOML() |
||||
if err != nil { |
||||
return true, false, err |
||||
} |
||||
b.body = append(b.body, enc...) |
||||
return true, false, nil |
||||
} |
||||
return false, false, nil |
||||
} |
||||
|
||||
func encodeTextMarshaler(buf []byte, v string) []byte { |
||||
// Emit the value without quotes if possible.
|
||||
if v == "true" || v == "false" { |
||||
return append(buf, v...) |
||||
} else if _, err := time.Parse(time.RFC3339Nano, v); err == nil { |
||||
return append(buf, v...) |
||||
} else if _, err := strconv.ParseInt(v, 10, 64); err == nil { |
||||
return append(buf, v...) |
||||
} else if _, err := strconv.ParseUint(v, 10, 64); err == nil { |
||||
return append(buf, v...) |
||||
} else if _, err := strconv.ParseFloat(v, 64); err == nil { |
||||
return append(buf, v...) |
||||
} |
||||
return strconv.AppendQuote(buf, v) |
||||
} |
||||
|
||||
func encodeMapKey(rv reflect.Value) (string, error) { |
||||
if rv.Kind() == reflect.String { |
||||
return rv.String(), nil |
||||
} |
||||
if tm, ok := rv.Interface().(encoding.TextMarshaler); ok { |
||||
b, err := tm.MarshalText() |
||||
return string(b), err |
||||
} |
||||
switch rv.Kind() { |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
return strconv.FormatInt(rv.Int(), 10), nil |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
||||
return strconv.FormatUint(rv.Uint(), 10), nil |
||||
} |
||||
return "", fmt.Errorf("toml: invalid map key type %v", rv.Type()) |
||||
} |
||||
|
||||
func isEmptyValue(v reflect.Value) bool { |
||||
switch v.Kind() { |
||||
case reflect.Array: |
||||
// encoding/json treats all arrays with non-zero length as non-empty. We check the
|
||||
// array content here because zero-length arrays are almost never used.
|
||||
len := v.Len() |
||||
for i := 0; i < len; i++ { |
||||
if !isEmptyValue(v.Index(i)) { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
case reflect.Map, reflect.Slice, reflect.String: |
||||
return v.Len() == 0 |
||||
case reflect.Bool: |
||||
return !v.Bool() |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
return v.Int() == 0 |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
||||
return v.Uint() == 0 |
||||
case reflect.Float32, reflect.Float64: |
||||
return v.Float() == 0 |
||||
case reflect.Interface, reflect.Ptr: |
||||
return v.IsNil() |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func quoteName(s string) string { |
||||
if len(s) == 0 { |
||||
return strconv.Quote(s) |
||||
} |
||||
for _, r := range s { |
||||
if r >= '0' && r <= '9' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r == '-' || r == '_' { |
||||
continue |
||||
} |
||||
return strconv.Quote(s) |
||||
} |
||||
return s |
||||
} |
@ -0,0 +1,107 @@ |
||||
package toml |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"reflect" |
||||
"strconv" |
||||
) |
||||
|
||||
var ( |
||||
errArrayMultiType = errors.New("array can't contain multiple types") |
||||
) |
||||
|
||||
// LineError is returned by Unmarshal, UnmarshalTable and Parse
|
||||
// if the error is local to a line.
|
||||
type LineError struct { |
||||
Line int |
||||
StructField string |
||||
Err error |
||||
} |
||||
|
||||
func (err *LineError) Error() string { |
||||
field := "" |
||||
if err.StructField != "" { |
||||
field = "(" + err.StructField + ") " |
||||
} |
||||
return fmt.Sprintf("line %d: %s%v", err.Line, field, err.Err) |
||||
} |
||||
|
||||
func lineError(line int, err error) error { |
||||
if err == nil { |
||||
return nil |
||||
} |
||||
if _, ok := err.(*LineError); ok { |
||||
return err |
||||
} |
||||
return &LineError{Line: line, Err: err} |
||||
} |
||||
|
||||
func lineErrorField(line int, field string, err error) error { |
||||
if lerr, ok := err.(*LineError); ok { |
||||
return lerr |
||||
} else if err != nil { |
||||
err = &LineError{Line: line, StructField: field, Err: err} |
||||
} |
||||
return err |
||||
} |
||||
|
||||
type overflowError struct { |
||||
kind reflect.Kind |
||||
v string |
||||
} |
||||
|
||||
func (err *overflowError) Error() string { |
||||
return fmt.Sprintf("value %s is out of range for %v", err.v, err.kind) |
||||
} |
||||
|
||||
func convertNumError(kind reflect.Kind, err error) error { |
||||
if numerr, ok := err.(*strconv.NumError); ok && numerr.Err == strconv.ErrRange { |
||||
return &overflowError{kind, numerr.Num} |
||||
} |
||||
return err |
||||
} |
||||
|
||||
type invalidUnmarshalError struct { |
||||
typ reflect.Type |
||||
} |
||||
|
||||
func (err *invalidUnmarshalError) Error() string { |
||||
if err.typ == nil { |
||||
return "toml: Unmarshal(nil)" |
||||
} |
||||
if err.typ.Kind() != reflect.Ptr { |
||||
return "toml: Unmarshal(non-pointer " + err.typ.String() + ")" |
||||
} |
||||
return "toml: Unmarshal(nil " + err.typ.String() + ")" |
||||
} |
||||
|
||||
type unmarshalTypeError struct { |
||||
what string |
||||
want string |
||||
typ reflect.Type |
||||
} |
||||
|
||||
func (err *unmarshalTypeError) Error() string { |
||||
msg := fmt.Sprintf("cannot unmarshal TOML %s into %s", err.what, err.typ) |
||||
if err.want != "" { |
||||
msg += " (need " + err.want + ")" |
||||
} |
||||
return msg |
||||
} |
||||
|
||||
type marshalNilError struct { |
||||
typ reflect.Type |
||||
} |
||||
|
||||
func (err *marshalNilError) Error() string { |
||||
return fmt.Sprintf("toml: cannot marshal nil %s", err.typ) |
||||
} |
||||
|
||||
type marshalTableError struct { |
||||
typ reflect.Type |
||||
} |
||||
|
||||
func (err *marshalTableError) Error() string { |
||||
return fmt.Sprintf("toml: cannot marshal %s as table, want struct or map type", err.typ) |
||||
} |
@ -0,0 +1,376 @@ |
||||
package toml |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/naoina/toml/ast" |
||||
) |
||||
|
||||
// The parser is generated by github.com/pointlander/peg. To regenerate it, do:
|
||||
//
|
||||
// go get -u github.com/pointlander/peg
|
||||
// go generate .
|
||||
|
||||
//go:generate peg -switch -inline parse.peg
|
||||
|
||||
var errParse = errors.New("invalid TOML syntax") |
||||
|
||||
// Parse returns an AST representation of TOML.
|
||||
// The toplevel is represented by a table.
|
||||
func Parse(data []byte) (*ast.Table, error) { |
||||
d := &parseState{p: &tomlParser{Buffer: string(data)}} |
||||
d.init() |
||||
|
||||
if err := d.parse(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return d.p.toml.table, nil |
||||
} |
||||
|
||||
type parseState struct { |
||||
p *tomlParser |
||||
} |
||||
|
||||
func (d *parseState) init() { |
||||
d.p.Init() |
||||
d.p.toml.init(d.p.buffer) |
||||
} |
||||
|
||||
func (d *parseState) parse() error { |
||||
if err := d.p.Parse(); err != nil { |
||||
if err, ok := err.(*parseError); ok { |
||||
return lineError(err.Line(), errParse) |
||||
} |
||||
return err |
||||
} |
||||
return d.execute() |
||||
} |
||||
|
||||
func (d *parseState) execute() (err error) { |
||||
defer func() { |
||||
if e := recover(); e != nil { |
||||
lerr, ok := e.(*LineError) |
||||
if !ok { |
||||
panic(e) |
||||
} |
||||
err = lerr |
||||
} |
||||
}() |
||||
d.p.Execute() |
||||
return nil |
||||
} |
||||
|
||||
func (e *parseError) Line() int { |
||||
tokens := []token32{e.max} |
||||
positions, p := make([]int, 2*len(tokens)), 0 |
||||
for _, token := range tokens { |
||||
positions[p], p = int(token.begin), p+1 |
||||
positions[p], p = int(token.end), p+1 |
||||
} |
||||
for _, t := range translatePositions(e.p.buffer, positions) { |
||||
if e.p.line < t.line { |
||||
e.p.line = t.line |
||||
} |
||||
} |
||||
return e.p.line |
||||
} |
||||
|
||||
type stack struct { |
||||
key string |
||||
table *ast.Table |
||||
} |
||||
|
||||
type array struct { |
||||
parent *array |
||||
child *array |
||||
current *ast.Array |
||||
line int |
||||
} |
||||
|
||||
type toml struct { |
||||
table *ast.Table |
||||
line int |
||||
currentTable *ast.Table |
||||
s string |
||||
key string |
||||
val ast.Value |
||||
arr *array |
||||
stack []*stack |
||||
skip bool |
||||
} |
||||
|
||||
func (p *toml) init(data []rune) { |
||||
p.line = 1 |
||||
p.table = p.newTable(ast.TableTypeNormal, "") |
||||
p.table.Position.End = len(data) - 1 |
||||
p.table.Data = data[:len(data)-1] // truncate the end_symbol added by PEG parse generator.
|
||||
p.currentTable = p.table |
||||
} |
||||
|
||||
func (p *toml) Error(err error) { |
||||
panic(lineError(p.line, err)) |
||||
} |
||||
|
||||
func (p *tomlParser) SetTime(begin, end int) { |
||||
p.val = &ast.Datetime{ |
||||
Position: ast.Position{Begin: begin, End: end}, |
||||
Data: p.buffer[begin:end], |
||||
Value: string(p.buffer[begin:end]), |
||||
} |
||||
} |
||||
|
||||
func (p *tomlParser) SetFloat64(begin, end int) { |
||||
p.val = &ast.Float{ |
||||
Position: ast.Position{Begin: begin, End: end}, |
||||
Data: p.buffer[begin:end], |
||||
Value: underscoreReplacer.Replace(string(p.buffer[begin:end])), |
||||
} |
||||
} |
||||
|
||||
func (p *tomlParser) SetInt64(begin, end int) { |
||||
p.val = &ast.Integer{ |
||||
Position: ast.Position{Begin: begin, End: end}, |
||||
Data: p.buffer[begin:end], |
||||
Value: underscoreReplacer.Replace(string(p.buffer[begin:end])), |
||||
} |
||||
} |
||||
|
||||
func (p *tomlParser) SetString(begin, end int) { |
||||
p.val = &ast.String{ |
||||
Position: ast.Position{Begin: begin, End: end}, |
||||
Data: p.buffer[begin:end], |
||||
Value: p.s, |
||||
} |
||||
p.s = "" |
||||
} |
||||
|
||||
func (p *tomlParser) SetBool(begin, end int) { |
||||
p.val = &ast.Boolean{ |
||||
Position: ast.Position{Begin: begin, End: end}, |
||||
Data: p.buffer[begin:end], |
||||
Value: string(p.buffer[begin:end]), |
||||
} |
||||
} |
||||
|
||||
func (p *tomlParser) StartArray() { |
||||
if p.arr == nil { |
||||
p.arr = &array{line: p.line, current: &ast.Array{}} |
||||
return |
||||
} |
||||
p.arr.child = &array{parent: p.arr, line: p.line, current: &ast.Array{}} |
||||
p.arr = p.arr.child |
||||
} |
||||
|
||||
func (p *tomlParser) AddArrayVal() { |
||||
if p.arr.current == nil { |
||||
p.arr.current = &ast.Array{} |
||||
} |
||||
p.arr.current.Value = append(p.arr.current.Value, p.val) |
||||
} |
||||
|
||||
func (p *tomlParser) SetArray(begin, end int) { |
||||
p.arr.current.Position = ast.Position{Begin: begin, End: end} |
||||
p.arr.current.Data = p.buffer[begin:end] |
||||
p.val = p.arr.current |
||||
p.arr = p.arr.parent |
||||
} |
||||
|
||||
func (p *toml) SetTable(buf []rune, begin, end int) { |
||||
p.setTable(p.table, buf, begin, end) |
||||
} |
||||
|
||||
func (p *toml) setTable(parent *ast.Table, buf []rune, begin, end int) { |
||||
name := string(buf[begin:end]) |
||||
names := splitTableKey(name) |
||||
parent, err := p.lookupTable(parent, names[:len(names)-1]) |
||||
if err != nil { |
||||
p.Error(err) |
||||
} |
||||
last := names[len(names)-1] |
||||
tbl := p.newTable(ast.TableTypeNormal, last) |
||||
switch v := parent.Fields[last].(type) { |
||||
case nil: |
||||
parent.Fields[last] = tbl |
||||
case []*ast.Table: |
||||
p.Error(fmt.Errorf("table `%s' is in conflict with array table in line %d", name, v[0].Line)) |
||||
case *ast.Table: |
||||
if (v.Position == ast.Position{}) { |
||||
// This table was created as an implicit parent.
|
||||
// Replace it with the real defined table.
|
||||
tbl.Fields = v.Fields |
||||
parent.Fields[last] = tbl |
||||
} else { |
||||
p.Error(fmt.Errorf("table `%s' is in conflict with table in line %d", name, v.Line)) |
||||
} |
||||
case *ast.KeyValue: |
||||
p.Error(fmt.Errorf("table `%s' is in conflict with line %d", name, v.Line)) |
||||
default: |
||||
p.Error(fmt.Errorf("BUG: table `%s' is in conflict but it's unknown type `%T'", last, v)) |
||||
} |
||||
p.currentTable = tbl |
||||
} |
||||
|
||||
func (p *toml) newTable(typ ast.TableType, name string) *ast.Table { |
||||
return &ast.Table{ |
||||
Line: p.line, |
||||
Name: name, |
||||
Type: typ, |
||||
Fields: make(map[string]interface{}), |
||||
} |
||||
} |
||||
|
||||
func (p *tomlParser) SetTableString(begin, end int) { |
||||
p.currentTable.Data = p.buffer[begin:end] |
||||
p.currentTable.Position.Begin = begin |
||||
p.currentTable.Position.End = end |
||||
} |
||||
|
||||
func (p *toml) SetArrayTable(buf []rune, begin, end int) { |
||||
p.setArrayTable(p.table, buf, begin, end) |
||||
} |
||||
|
||||
func (p *toml) setArrayTable(parent *ast.Table, buf []rune, begin, end int) { |
||||
name := string(buf[begin:end]) |
||||
names := splitTableKey(name) |
||||
parent, err := p.lookupTable(parent, names[:len(names)-1]) |
||||
if err != nil { |
||||
p.Error(err) |
||||
} |
||||
last := names[len(names)-1] |
||||
tbl := p.newTable(ast.TableTypeArray, last) |
||||
switch v := parent.Fields[last].(type) { |
||||
case nil: |
||||
parent.Fields[last] = []*ast.Table{tbl} |
||||
case []*ast.Table: |
||||
parent.Fields[last] = append(v, tbl) |
||||
case *ast.Table: |
||||
p.Error(fmt.Errorf("array table `%s' is in conflict with table in line %d", name, v.Line)) |
||||
case *ast.KeyValue: |
||||
p.Error(fmt.Errorf("array table `%s' is in conflict with line %d", name, v.Line)) |
||||
default: |
||||
p.Error(fmt.Errorf("BUG: array table `%s' is in conflict but it's unknown type `%T'", name, v)) |
||||
} |
||||
p.currentTable = tbl |
||||
} |
||||
|
||||
func (p *toml) StartInlineTable() { |
||||
p.skip = false |
||||
p.stack = append(p.stack, &stack{p.key, p.currentTable}) |
||||
buf := []rune(p.key) |
||||
if p.arr == nil { |
||||
p.setTable(p.currentTable, buf, 0, len(buf)) |
||||
} else { |
||||
p.setArrayTable(p.currentTable, buf, 0, len(buf)) |
||||
} |
||||
} |
||||
|
||||
func (p *toml) EndInlineTable() { |
||||
st := p.stack[len(p.stack)-1] |
||||
p.key, p.currentTable = st.key, st.table |
||||
p.stack[len(p.stack)-1] = nil |
||||
p.stack = p.stack[:len(p.stack)-1] |
||||
p.skip = true |
||||
} |
||||
|
||||
func (p *toml) AddLineCount(i int) { |
||||
p.line += i |
||||
} |
||||
|
||||
func (p *toml) SetKey(buf []rune, begin, end int) { |
||||
p.key = string(buf[begin:end]) |
||||
} |
||||
|
||||
func (p *toml) AddKeyValue() { |
||||
if p.skip { |
||||
p.skip = false |
||||
return |
||||
} |
||||
if val, exists := p.currentTable.Fields[p.key]; exists { |
||||
switch v := val.(type) { |
||||
case *ast.Table: |
||||
p.Error(fmt.Errorf("key `%s' is in conflict with table in line %d", p.key, v.Line)) |
||||
case *ast.KeyValue: |
||||
p.Error(fmt.Errorf("key `%s' is in conflict with line %xd", p.key, v.Line)) |
||||
default: |
||||
p.Error(fmt.Errorf("BUG: key `%s' is in conflict but it's unknown type `%T'", p.key, v)) |
||||
} |
||||
} |
||||
p.currentTable.Fields[p.key] = &ast.KeyValue{Key: p.key, Value: p.val, Line: p.line} |
||||
} |
||||
|
||||
func (p *toml) SetBasicString(buf []rune, begin, end int) { |
||||
p.s = p.unquote(string(buf[begin:end])) |
||||
} |
||||
|
||||
func (p *toml) SetMultilineString() { |
||||
p.s = p.unquote(`"` + escapeReplacer.Replace(strings.TrimLeft(p.s, "\r\n")) + `"`) |
||||
} |
||||
|
||||
func (p *toml) AddMultilineBasicBody(buf []rune, begin, end int) { |
||||
p.s += string(buf[begin:end]) |
||||
} |
||||
|
||||
func (p *toml) SetLiteralString(buf []rune, begin, end int) { |
||||
p.s = string(buf[begin:end]) |
||||
} |
||||
|
||||
func (p *toml) SetMultilineLiteralString(buf []rune, begin, end int) { |
||||
p.s = strings.TrimLeft(string(buf[begin:end]), "\r\n") |
||||
} |
||||
|
||||
func (p *toml) unquote(s string) string { |
||||
s, err := strconv.Unquote(s) |
||||
if err != nil { |
||||
p.Error(err) |
||||
} |
||||
return s |
||||
} |
||||
|
||||
func (p *toml) lookupTable(t *ast.Table, keys []string) (*ast.Table, error) { |
||||
for _, s := range keys { |
||||
val, exists := t.Fields[s] |
||||
if !exists { |
||||
tbl := p.newTable(ast.TableTypeNormal, s) |
||||
t.Fields[s] = tbl |
||||
t = tbl |
||||
continue |
||||
} |
||||
switch v := val.(type) { |
||||
case *ast.Table: |
||||
t = v |
||||
case []*ast.Table: |
||||
t = v[len(v)-1] |
||||
case *ast.KeyValue: |
||||
return nil, fmt.Errorf("key `%s' is in conflict with line %d", s, v.Line) |
||||
default: |
||||
return nil, fmt.Errorf("BUG: key `%s' is in conflict but it's unknown type `%T'", s, v) |
||||
} |
||||
} |
||||
return t, nil |
||||
} |
||||
|
||||
func splitTableKey(tk string) []string { |
||||
key := make([]byte, 0, 1) |
||||
keys := make([]string, 0, 1) |
||||
inQuote := false |
||||
for i := 0; i < len(tk); i++ { |
||||
k := tk[i] |
||||
switch { |
||||
case k == tableSeparator && !inQuote: |
||||
keys = append(keys, string(key)) |
||||
key = key[:0] // reuse buffer.
|
||||
case k == '"': |
||||
inQuote = !inQuote |
||||
case (k == ' ' || k == '\t') && !inQuote: |
||||
// skip.
|
||||
default: |
||||
key = append(key, k) |
||||
} |
||||
} |
||||
keys = append(keys, string(key)) |
||||
return keys |
||||
} |
@ -0,0 +1,145 @@ |
||||
package toml |
||||
|
||||
type tomlParser Peg { |
||||
toml |
||||
} |
||||
|
||||
TOML <- Expression (newline Expression)* newline? !. { _ = buffer } |
||||
|
||||
Expression <- ( |
||||
<ws table ws comment? (wsnl keyval ws comment?)*> { p.SetTableString(begin, end) } |
||||
/ ws keyval ws comment? |
||||
/ ws comment? |
||||
/ ws |
||||
) |
||||
|
||||
newline <- <[\r\n]+> { p.AddLineCount(end - begin) } |
||||
|
||||
ws <- [ \t]* |
||||
wsnl <- ( |
||||
[ \t] |
||||
/ <[\r\n]> { p.AddLineCount(end - begin) } |
||||
)* |
||||
|
||||
comment <- '#' <[\t -\0x10FFFF]*> |
||||
|
||||
keyval <- key ws '=' ws val { p.AddKeyValue() } |
||||
|
||||
key <- bareKey / quotedKey |
||||
|
||||
bareKey <- <[0-9A-Za-z\-_]+> { p.SetKey(p.buffer, begin, end) } |
||||
|
||||
quotedKey <- '"' <basicChar+> '"' { p.SetKey(p.buffer, begin-1, end+1) } |
||||
|
||||
val <- ( |
||||
<datetime> { p.SetTime(begin, end) } |
||||
/ <float> { p.SetFloat64(begin, end) } |
||||
/ <integer> { p.SetInt64(begin, end) } |
||||
/ <string> { p.SetString(begin, end) } |
||||
/ <boolean> { p.SetBool(begin, end) } |
||||
/ <array> { p.SetArray(begin, end) } |
||||
/ inlineTable |
||||
) |
||||
|
||||
table <- stdTable / arrayTable |
||||
|
||||
stdTable <- '[' ws <tableKey> ws ']' { p.SetTable(p.buffer, begin, end) } |
||||
|
||||
arrayTable <- '[[' ws <tableKey> ws ']]' { p.SetArrayTable(p.buffer, begin, end) } |
||||
|
||||
inlineTable <- ( |
||||
'{' { p.StartInlineTable() } |
||||
ws inlineTableKeyValues ws |
||||
'}' { p.EndInlineTable() } |
||||
) |
||||
|
||||
inlineTableKeyValues <- (keyval inlineTableValSep?)* |
||||
|
||||
tableKey <- key (tableKeySep key)* |
||||
|
||||
tableKeySep <- ws '.' ws |
||||
|
||||
inlineTableValSep <- ws ',' ws |
||||
|
||||
integer <- [\-+]? int |
||||
int <- [1-9] (digit / '_' digit)+ / digit |
||||
|
||||
float <- integer (frac exp? / frac? exp) |
||||
frac <- '.' digit (digit / '_' digit)* |
||||
exp <- [eE] [\-+]? digit (digit / '_' digit)* |
||||
|
||||
string <- ( |
||||
mlLiteralString |
||||
/ literalString |
||||
/ mlBasicString |
||||
/ basicString |
||||
) |
||||
|
||||
basicString <- <'"' basicChar* '"'> { p.SetBasicString(p.buffer, begin, end) } |
||||
|
||||
basicChar <- basicUnescaped / escaped |
||||
escaped <- escape ([btnfr"/\\] / 'u' hexQuad / 'U' hexQuad hexQuad) |
||||
|
||||
basicUnescaped <- [ -!#-\[\]-\0x10FFFF] |
||||
|
||||
escape <- '\\' |
||||
|
||||
mlBasicString <- '"""' mlBasicBody '"""' { p.SetMultilineString() } |
||||
|
||||
mlBasicBody <- ( |
||||
<basicChar / newline> { p.AddMultilineBasicBody(p.buffer, begin, end) } |
||||
/ escape newline wsnl |
||||
)* |
||||
|
||||
literalString <- "'" <literalChar*> "'" { p.SetLiteralString(p.buffer, begin, end) } |
||||
|
||||
literalChar <- [\t -&(-\0x10FFFF] |
||||
|
||||
mlLiteralString <- "'''" <mlLiteralBody> "'''" { p.SetMultilineLiteralString(p.buffer, begin, end) } |
||||
|
||||
mlLiteralBody <- (!"'''" (mlLiteralChar / newline))* |
||||
|
||||
mlLiteralChar <- [\t -\0x10FFFF] |
||||
|
||||
hexdigit <- [0-9A-Fa-f] |
||||
hexQuad <- hexdigit hexdigit hexdigit hexdigit |
||||
|
||||
boolean <- 'true' / 'false' |
||||
|
||||
dateFullYear <- digitQuad |
||||
dateMonth <- digitDual |
||||
dateMDay <- digitDual |
||||
timeHour <- digitDual |
||||
timeMinute <- digitDual |
||||
timeSecond <- digitDual |
||||
timeSecfrac <- '.' digit+ |
||||
timeNumoffset <- [\-+] timeHour ':' timeMinute |
||||
timeOffset <- 'Z' / timeNumoffset |
||||
partialTime <- timeHour ':' timeMinute ':' timeSecond timeSecfrac? |
||||
fullDate <- dateFullYear '-' dateMonth '-' dateMDay |
||||
fullTime <- partialTime timeOffset |
||||
datetime <- (fullDate ('T' fullTime)?) / partialTime |
||||
|
||||
digit <- [0-9] |
||||
digitDual <- digit digit |
||||
digitQuad <- digitDual digitDual |
||||
|
||||
array <- ( |
||||
'[' { p.StartArray() } |
||||
wsnl arrayValues? wsnl |
||||
']' |
||||
) |
||||
|
||||
arrayValues <- ( |
||||
val { p.AddArrayVal() } |
||||
( |
||||
wsnl comment? |
||||
wsnl arraySep |
||||
wsnl comment? |
||||
wsnl val { p.AddArrayVal() } |
||||
)* |
||||
wsnl arraySep? |
||||
wsnl comment? |
||||
) |
||||
|
||||
arraySep <- ',' |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,65 @@ |
||||
package toml |
||||
|
||||
import ( |
||||
"fmt" |
||||
"reflect" |
||||
"strings" |
||||
) |
||||
|
||||
const fieldTagName = "toml" |
||||
|
||||
// fieldCache maps normalized field names to their position in a struct.
|
||||
type fieldCache struct { |
||||
named map[string]fieldInfo // fields with an explicit name in tag
|
||||
auto map[string]fieldInfo // fields with auto-assigned normalized names
|
||||
} |
||||
|
||||
type fieldInfo struct { |
||||
index []int |
||||
name string |
||||
ignored bool |
||||
} |
||||
|
||||
func makeFieldCache(cfg *Config, rt reflect.Type) fieldCache { |
||||
named, auto := make(map[string]fieldInfo), make(map[string]fieldInfo) |
||||
for i := 0; i < rt.NumField(); i++ { |
||||
ft := rt.Field(i) |
||||
// skip unexported fields
|
||||
if ft.PkgPath != "" && !ft.Anonymous { |
||||
continue |
||||
} |
||||
col, _ := extractTag(ft.Tag.Get(fieldTagName)) |
||||
info := fieldInfo{index: ft.Index, name: ft.Name, ignored: col == "-"} |
||||
if col == "" || col == "-" { |
||||
auto[cfg.NormFieldName(rt, ft.Name)] = info |
||||
} else { |
||||
named[col] = info |
||||
} |
||||
} |
||||
return fieldCache{named, auto} |
||||
} |
||||
|
||||
func (fc fieldCache) findField(cfg *Config, rv reflect.Value, name string) (reflect.Value, string, error) { |
||||
info, found := fc.named[name] |
||||
if !found { |
||||
info, found = fc.auto[cfg.NormFieldName(rv.Type(), name)] |
||||
} |
||||
if !found { |
||||
if cfg.MissingField == nil { |
||||
return reflect.Value{}, "", fmt.Errorf("field corresponding to `%s' is not defined in %v", name, rv.Type()) |
||||
} else { |
||||
return reflect.Value{}, "", cfg.MissingField(rv.Type(), name) |
||||
} |
||||
} else if info.ignored { |
||||
return reflect.Value{}, "", fmt.Errorf("field corresponding to `%s' in %v cannot be set through TOML", name, rv.Type()) |
||||
} |
||||
return rv.FieldByIndex(info.index), info.name, nil |
||||
} |
||||
|
||||
func extractTag(tag string) (col, rest string) { |
||||
tags := strings.SplitN(tag, ",", 2) |
||||
if len(tags) == 2 { |
||||
return strings.TrimSpace(tags[0]), strings.TrimSpace(tags[1]) |
||||
} |
||||
return strings.TrimSpace(tags[0]), "" |
||||
} |
Loading…
Reference in new issue