diff --git a/Gopkg.lock b/Gopkg.lock index d8d5dfb..f59ad8a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,14 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + digest = "1:9f3b30d9f8e0d7040f729b82dcbc8f0dead820a133b3147ce355fc451f32d761" + name = "github.com/BurntSushi/toml" + packages = ["."] + pruneopts = "UT" + revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005" + version = "v0.3.1" + [[projects]] digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" name = "github.com/davecgh/go-spew" @@ -216,14 +224,18 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ + "github.com/BurntSushi/toml", "github.com/dgraph-io/dgo", "github.com/dgraph-io/dgo/protos/api", "github.com/graph-gophers/graphql-go", + "github.com/graph-gophers/graphql-go/errors", "github.com/pkg/errors", "github.com/satori/go.uuid", "github.com/stretchr/testify/require", "golang.org/x/crypto/bcrypt", "google.golang.org/grpc", + "google.golang.org/grpc/codes", + "google.golang.org/grpc/status", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/api/apiServer.go b/api/apiServer.go index 1db7919..c6ce167 100644 --- a/api/apiServer.go +++ b/api/apiServer.go @@ -6,8 +6,8 @@ import ( "sync" "github.com/pkg/errors" + "github.com/romshark/dgraph_graphql_go/api/config" "github.com/romshark/dgraph_graphql_go/api/graph" - "github.com/romshark/dgraph_graphql_go/api/options" "github.com/romshark/dgraph_graphql_go/api/transport" "github.com/romshark/dgraph_graphql_go/api/validator" "github.com/romshark/dgraph_graphql_go/store" @@ -29,7 +29,7 @@ type Server interface { } type server struct { - opts options.ServerOptions + conf *config.ServerConfig store store.Store graph *graph.Graph debugSessionKey []byte @@ -38,15 +38,15 @@ type server struct { } // NewServer creates a new API server instance -func NewServer(opts options.ServerOptions) (Server, error) { - if err := opts.Prepare(); err != nil { - return nil, fmt.Errorf("options: %s", err) +func NewServer(conf *config.ServerConfig) (Server, error) { + if err := conf.Prepare(); err != nil { + return nil, fmt.Errorf("config: %s", err) } // Initialize validator validator, err := validator.NewValidator( - opts.Mode == options.ModeProduction, - validator.Options{ + conf.Mode == config.ModeProduction, + validator.Config{ PasswordLenMin: 6, PasswordLenMax: 256, EmailLenMax: 96, @@ -66,22 +66,22 @@ func NewServer(opts options.ServerOptions) (Server, error) { // Initialize store instance store := dgraph.NewStore( - opts.DBHost, + conf.DBHost, // Compare password func(hash, password string) bool { - return opts.PasswordHasher.Compare([]byte(hash), []byte(password)) + return conf.PasswordHasher.Compare([]byte(hash), []byte(password)) }, - opts.DebugLog, - opts.ErrorLog, + conf.DebugLog, + conf.ErrorLog, ) graph, err := graph.New( store, validator, - opts.SessionKeyGenerator, - opts.PasswordHasher, + conf.SessionKeyGenerator, + conf.PasswordHasher, ) if err != nil { return nil, errors.Wrap(err, "graph init") @@ -90,37 +90,37 @@ func NewServer(opts options.ServerOptions) (Server, error) { // Initialize API server instance newSrv := &server{ store: store, - opts: opts, + conf: conf, graph: graph, - transports: opts.Transport, + transports: conf.Transport, shutdownAwaitBlocker: &sync.WaitGroup{}, } // Initialize transports - for _, transport := range opts.Transport { + for _, transport := range conf.Transport { if err := transport.Init( newSrv.onGraphQuery, newSrv.onAuth, newSrv.onDebugAuth, newSrv.onDebugSess, - opts.DebugLog, - opts.ErrorLog, + conf.DebugLog, + conf.ErrorLog, ); err != nil { return nil, err } } - opts.DebugLog.Print("all transports initialized") + conf.DebugLog.Print("all transports initialized") // Generate the debug user session key if the debug user is enabled - if opts.DebugUser.Status != options.DebugUserDisabled { - newSrv.debugSessionKey = []byte(opts.SessionKeyGenerator.Generate()) + if conf.DebugUser.Mode != config.DebugUserDisabled { + newSrv.debugSessionKey = []byte(conf.SessionKeyGenerator.Generate()) } return newSrv, nil } func (srv *server) logErrf(format string, v ...interface{}) { - srv.opts.ErrorLog.Printf(format, v...) + srv.conf.ErrorLog.Printf(format, v...) } // Launch implements the Server interface diff --git a/api/options/options_test.go b/api/config/config_test.go similarity index 51% rename from api/options/options_test.go rename to api/config/config_test.go index 6330a13..07e7e88 100644 --- a/api/options/options_test.go +++ b/api/config/config_test.go @@ -1,34 +1,34 @@ -package options_test +package config_test import ( "testing" - "github.com/romshark/dgraph_graphql_go/api/options" + "github.com/romshark/dgraph_graphql_go/api/config" "github.com/romshark/dgraph_graphql_go/api/transport" thttp "github.com/romshark/dgraph_graphql_go/api/transport/http" "github.com/stretchr/testify/require" ) -func TestOptionsInvalid(t *testing.T) { - assumeErr := func(t *testing.T, opts options.ServerOptions) { - require.Error(t, opts.Prepare()) +func TestConfigInvalid(t *testing.T) { + assumeErr := func(t *testing.T, conf config.ServerConfig) { + require.Error(t, conf.Prepare()) } t.Run("noTransport", func(t *testing.T) { - assumeErr(t, options.ServerOptions{ - Mode: options.ModeProduction, + assumeErr(t, config.ServerConfig{ + Mode: config.ModeProduction, Transport: []transport.Server{}, }) }) t.Run("production/debugUserEnabled", func(t *testing.T) { - debugUsrOptions := []options.DebugUserStatus{ - options.DebugUserRW, - options.DebugUserReadOnly, + debugUserModes := []config.DebugUserMode{ + config.DebugUserRW, + config.DebugUserReadOnly, } - for _, debugUsrOption := range debugUsrOptions { - t.Run(string(debugUsrOption), func(t *testing.T) { - serverHTTP, err := thttp.NewServer(thttp.ServerOptions{ + for _, debugUserMode := range debugUserModes { + t.Run(string(debugUserMode), func(t *testing.T) { + serverHTTP, err := thttp.NewServer(thttp.ServerConfig{ Host: "localhost:80", TLS: &thttp.ServerTLS{ CertificateFilePath: "certfile", @@ -39,11 +39,11 @@ func TestOptionsInvalid(t *testing.T) { require.NoError(t, err) require.NotNil(t, serverHTTP) - assumeErr(t, options.ServerOptions{ - Mode: options.ModeProduction, + assumeErr(t, config.ServerConfig{ + Mode: config.ModeProduction, Transport: []transport.Server{serverHTTP}, - DebugUser: options.DebugUserOptions{ - Status: debugUsrOption, + DebugUser: config.DebugUserConfig{ + Mode: debugUserMode, }, }) }) @@ -51,7 +51,7 @@ func TestOptionsInvalid(t *testing.T) { }) t.Run("production/nonTLSTransport", func(t *testing.T) { - serverHTTP, err := thttp.NewServer(thttp.ServerOptions{ + serverHTTP, err := thttp.NewServer(thttp.ServerConfig{ Host: "localhost:80", TLS: nil, Playground: true, @@ -59,8 +59,8 @@ func TestOptionsInvalid(t *testing.T) { require.NoError(t, err) require.NotNil(t, serverHTTP) - assumeErr(t, options.ServerOptions{ - Mode: options.ModeProduction, + assumeErr(t, config.ServerConfig{ + Mode: config.ModeProduction, Transport: []transport.Server{serverHTTP}, }) }) diff --git a/api/config/debugUserConfig.go b/api/config/debugUserConfig.go new file mode 100644 index 0000000..016b22f --- /dev/null +++ b/api/config/debugUserConfig.go @@ -0,0 +1,46 @@ +package config + +import "errors" + +// DebugUserConfig defines the API debug user configurations +type DebugUserConfig struct { + Mode DebugUserMode + Username string + Password string +} + +// Prepares sets defaults and validates the configurations +func (conf *DebugUserConfig) Prepares(mode Mode) error { + // Set default debug user mode + if conf.Mode == DebugUserUnset { + switch mode { + case ModeProduction: + conf.Mode = DebugUserDisabled + case ModeBeta: + conf.Mode = DebugUserReadOnly + default: + conf.Mode = DebugUserRW + } + } + + // Use "debug" as the default debug username + if conf.Username == "" { + conf.Username = "debug" + } + + // Use "debug" as the default debug password + if conf.Password == "" { + conf.Password = "debug" + } + + // VALIDATE + + // Ensure the debug user isn't enabled in production mode + if mode == ModeProduction { + if conf.Mode != DebugUserDisabled { + return errors.New("debug user must be disabled in production mode") + } + } + + return nil +} diff --git a/api/config/debugUserMode.go b/api/config/debugUserMode.go new file mode 100644 index 0000000..53568ca --- /dev/null +++ b/api/config/debugUserMode.go @@ -0,0 +1,35 @@ +package config + +import "fmt" + +// DebugUserMode represents a debug user mode +type DebugUserMode string + +const ( + // DebugUserUnset represents the default unset option value + DebugUserUnset DebugUserMode = "" + + // DebugUserDisabled disables the debug user + DebugUserDisabled DebugUserMode = "disabled" + + // DebugUserReadOnly enables the debug user in a read-only mode + DebugUserReadOnly DebugUserMode = "read-only" + + // DebugUserRW enables the debug user in a read-write mode + DebugUserRW DebugUserMode = "read-write" +) + +// Validate returns an error if the value is invalid +func (md DebugUserMode) Validate() error { + switch md { + case DebugUserUnset: + fallthrough + case DebugUserDisabled: + fallthrough + case DebugUserRW: + fallthrough + case DebugUserReadOnly: + return nil + } + return fmt.Errorf("invalid debug user mode: '%s'", md) +} diff --git a/api/config/duration.go b/api/config/duration.go new file mode 100644 index 0000000..b0dcd2d --- /dev/null +++ b/api/config/duration.go @@ -0,0 +1,26 @@ +package config + +import ( + "fmt" + "reflect" + "time" +) + +// Duration represents a duration +type Duration time.Duration + +// UnmarshalTOML implements the TOML unmarshaler interface +func (v *Duration) UnmarshalTOML(val interface{}) error { + if str, isString := val.(string); isString { + dur, err := time.ParseDuration(str) + if err != nil { + return err + } + *v = Duration(dur) + return nil + } + return fmt.Errorf( + "unexpected duration value type: %s", + reflect.TypeOf(val), + ) +} diff --git a/api/config/file.go b/api/config/file.go new file mode 100644 index 0000000..cb9478f --- /dev/null +++ b/api/config/file.go @@ -0,0 +1,234 @@ +package config + +import ( + "crypto/tls" + "fmt" + "io" + "log" + "os" + "strings" + "time" + + "github.com/BurntSushi/toml" + "github.com/pkg/errors" + "github.com/romshark/dgraph_graphql_go/api/passhash" + "github.com/romshark/dgraph_graphql_go/api/sesskeygen" + thttp "github.com/romshark/dgraph_graphql_go/api/transport/http" +) + +// File represents a TOML encoded configuration file +type File struct { + Mode Mode `toml:"mode"` + PasswordHasher PasswordHasher `toml:"password-hasher"` + SessionKeyGenerator SessionKeyGenerator `toml:"session-key-generator"` + DB struct { + Host string `toml:"host"` + } `toml:"db"` + Log struct { + Debug string `toml:"debug"` + Error string `toml:"error"` + } `toml:"log"` + Debug struct { + Mode string `toml:"mode"` + Username string `toml:"username"` + Password string `toml:"password"` + } `toml:"debug"` + TransportHTTP struct { + Host string `toml:"host"` + KeepAliveDuration Duration `toml:"keep-alive-duration"` + Playground bool `toml:"playground"` + TLS struct { + Enabled bool `toml:"enabled"` + MinVersion TLSVersion `toml:"min-version"` + CertificateFile string `toml:"certificate-file"` + KeyFile string `toml:"key-file"` + CurvePreferences []TLSCurveID `toml:"curve-preferences"` + CipherSuites []TLSCipherSuite `toml:"cipher-suites"` + } `toml:"tls"` + } `toml:"transport-http"` +} + +func (f *File) mode(conf *ServerConfig) error { + if err := f.Mode.Validate(); err != nil { + return err + } + conf.Mode = f.Mode + return nil +} + +func (f *File) dbHost(conf *ServerConfig) error { + conf.DBHost = f.DB.Host + return nil +} + +func (f *File) passwordHasher(conf *ServerConfig) error { + switch f.PasswordHasher { + case "bcrypt": + conf.PasswordHasher = passhash.Bcrypt{} + return nil + } + return fmt.Errorf("unsupported password hasher: '%s'", f.PasswordHasher) +} + +func (f *File) sessionKeyGenerator(conf *ServerConfig) error { + switch f.SessionKeyGenerator { + case "default": + conf.SessionKeyGenerator = sesskeygen.NewDefault() + return nil + } + return fmt.Errorf( + "unsupported session key generator: '%s'", + f.SessionKeyGenerator, + ) +} + +func (f *File) debugLog(conf *ServerConfig) error { + var writer io.Writer + if strings.HasPrefix(f.Log.Debug, "stdout") { + writer = os.Stdout + } else if strings.HasPrefix(f.Log.Debug, "file:") && + len(f.Log.Debug) > 5 { + // Debug log to file + var err error + writer, err = os.OpenFile( + f.Log.Debug[5:], + os.O_WRONLY|os.O_APPEND|os.O_CREATE, + 0660, + ) + if err != nil { + return errors.Wrap(err, "debug log file") + } + } else { + return fmt.Errorf("invalid: '%s'", f.Log.Debug) + } + conf.DebugLog = log.New( + writer, + "DBG: ", + log.Ldate|log.Ltime, + ) + return nil +} + +func (f *File) errorLog(conf *ServerConfig) error { + var writer io.Writer + if strings.HasPrefix(f.Log.Error, "stderr") { + writer = os.Stdout + } else if strings.HasPrefix(f.Log.Error, "file:") && + len(f.Log.Error) > 5 { + // Error log to file + var err error + writer, err = os.OpenFile( + f.Log.Error[5:], + os.O_WRONLY|os.O_APPEND|os.O_CREATE, + 0660, + ) + if err != nil { + return errors.Wrap(err, "error log file") + } + } else { + return fmt.Errorf("invalid: '%s'", f.Log.Error) + } + conf.ErrorLog = log.New( + writer, + "ERR: ", + log.Ldate|log.Ltime, + ) + return nil +} + +func (f *File) debug(conf *ServerConfig) error { + conf.DebugUser.Mode = DebugUserMode(f.Debug.Mode) + if err := conf.DebugUser.Mode.Validate(); err != nil { + return errors.Wrap(err, "debug user mode") + } + conf.DebugUser.Username = f.Debug.Username + conf.DebugUser.Password = f.Debug.Password + return nil +} + +func (f *File) transportHTTP(conf *ServerConfig) error { + srvConf := thttp.ServerConfig{} + + // Host + if len(f.TransportHTTP.Host) < 1 { + return nil + } + srvConf.Host = f.TransportHTTP.Host + + // Keep-alive duration + srvConf.KeepAliveDuration = time.Duration(f.TransportHTTP.KeepAliveDuration) + + // TLS + if f.TransportHTTP.TLS.Enabled { + srvConf.TLS = &thttp.ServerTLS{ + Config: &tls.Config{}, + CertificateFilePath: f.TransportHTTP.TLS.CertificateFile, + PrivateKeyFilePath: f.TransportHTTP.TLS.KeyFile, + } + + // Min version + srvConf.TLS.Config.MinVersion = uint16(f.TransportHTTP.TLS.MinVersion) + + // Curve preferences + curveIDs := make( + []tls.CurveID, + len(f.TransportHTTP.TLS.CurvePreferences), + ) + for i, curveID := range f.TransportHTTP.TLS.CurvePreferences { + curveIDs[i] = tls.CurveID(curveID) + } + srvConf.TLS.Config.CurvePreferences = curveIDs + srvConf.TLS.Config.PreferServerCipherSuites = true + + // Cipher suites + cipherSuites := make([]uint16, len(f.TransportHTTP.TLS.CipherSuites)) + for i, cipherSuite := range f.TransportHTTP.TLS.CipherSuites { + cipherSuites[i] = uint16(cipherSuite) + } + srvConf.TLS.Config.CipherSuites = cipherSuites + } + + // Playground + srvConf.Playground = f.TransportHTTP.Playground + + newServer, err := thttp.NewServer(srvConf) + if err != nil { + return errors.Wrap(err, "HTTP server init") + } + + conf.Transport = append(conf.Transport, newServer) + + return nil +} + +// FromFile reads the configuration from a file +func FromFile(path string) (*ServerConfig, error) { + var file File + conf := &ServerConfig{} + + // Read TOML config file + if _, err := toml.DecodeFile(path, &file); err != nil { + return nil, errors.Wrap(err, "TOML decode") + } + + for setterName, setter := range map[string]func(*ServerConfig) error{ + "mode": file.mode, + "db.host": file.dbHost, + "password-hasher": file.passwordHasher, + "session-key-generator": file.sessionKeyGenerator, + "log.debug": file.debugLog, + "log.error": file.errorLog, + "debug": file.debug, + "transport-http": file.transportHTTP, + } { + if err := setter(conf); err != nil { + return nil, errors.Wrap(err, setterName) + } + } + + if err := conf.Prepare(); err != nil { + return nil, err + } + + return conf, nil +} diff --git a/api/config/mode.go b/api/config/mode.go new file mode 100644 index 0000000..52a7b04 --- /dev/null +++ b/api/config/mode.go @@ -0,0 +1,30 @@ +package config + +import "fmt" + +// Mode defines the server mode +type Mode string + +const ( + // ModeDebug represents the debug server mode + ModeDebug Mode = "debug" + + // ModeBeta represents the beta server mode + ModeBeta Mode = "beta" + + // ModeProduction represents the production server mode + ModeProduction Mode = "production" +) + +// Validate returns an error if the mode is unknown +func (mode Mode) Validate() error { + switch mode { + case ModeDebug: + fallthrough + case ModeBeta: + fallthrough + case ModeProduction: + return nil + } + return fmt.Errorf("unknown mode: '%s'", mode) +} diff --git a/api/config/passwordHasher.go b/api/config/passwordHasher.go new file mode 100644 index 0000000..9b01122 --- /dev/null +++ b/api/config/passwordHasher.go @@ -0,0 +1,26 @@ +package config + +import ( + "fmt" + "reflect" +) + +// PasswordHasher represents a password hasher identifier +type PasswordHasher string + +// UnmarshalTOML implements the TOML unmarshaler interface +func (v *PasswordHasher) UnmarshalTOML(val interface{}) error { + if str, isString := val.(string); isString { + switch str { + case "bcrypt": + *v = PasswordHasher(str) + default: + return fmt.Errorf("unknown password hasher: '%s'", val) + } + return nil + } + return fmt.Errorf( + "unexpected password hasher value type: %s", + reflect.TypeOf(val), + ) +} diff --git a/api/config/serverConfig.go b/api/config/serverConfig.go new file mode 100644 index 0000000..f2c1828 --- /dev/null +++ b/api/config/serverConfig.go @@ -0,0 +1,118 @@ +package config + +import ( + "errors" + "log" + "os" + + "github.com/romshark/dgraph_graphql_go/api/passhash" + "github.com/romshark/dgraph_graphql_go/api/sesskeygen" + "github.com/romshark/dgraph_graphql_go/api/transport" + thttp "github.com/romshark/dgraph_graphql_go/api/transport/http" +) + +// ServerConfig defines the API server configurations +type ServerConfig struct { + Mode Mode + DBHost string + SessionKeyGenerator sesskeygen.SessionKeyGenerator + PasswordHasher passhash.PasswordHasher + DebugUser DebugUserConfig + Transport []transport.Server + DebugLog *log.Logger + ErrorLog *log.Logger +} + +// Prepare sets defaults and validates the configurations +func (conf *ServerConfig) Prepare() error { + // Use production mode by default + if conf.Mode == "" { + conf.Mode = ModeProduction + } + + // Set default database host address + if conf.DBHost == "" { + switch conf.Mode { + case ModeProduction: + conf.DBHost = "localhost:9080" + default: + conf.DBHost = "localhost:10180" + } + } + + // Use default session key generator + if conf.SessionKeyGenerator == nil { + conf.SessionKeyGenerator = sesskeygen.NewDefault() + } + + // Use default password hasher + if conf.PasswordHasher == nil { + conf.PasswordHasher = passhash.Bcrypt{} + } + + // Use default debug logger to stdout + if conf.DebugLog == nil { + conf.DebugLog = log.New( + os.Stdout, + "DBG: ", + log.Ldate|log.Ltime, + ) + } + + // Use default error logger to stderr + if conf.ErrorLog == nil { + conf.ErrorLog = log.New( + os.Stderr, + "ERR: ", + log.Ldate|log.Ltime, + ) + } + + // VALIDATE + + // Ensure at least one transport adapter is specified + if len(conf.Transport) < 1 { + return errors.New("no transport adapter") + } + + if conf.Mode == ModeProduction { + // Validate transport adapters + for _, trn := range conf.Transport { + if httpAdapter, ok := trn.(*thttp.Server); ok { + // Ensure TLS is enabled in production on all transport adapters + conf := httpAdapter.Config() + if conf.TLS == nil { + return errors.New( + "TLS must not be disabled on HTTP transport adapter " + + "in production mode", + ) + } + + // Ensure playground is disabled in production + if conf.Playground { + return errors.New( + "the playground must be disabled on " + + "HTTP transport adapter in production mode", + ) + } + } + } + + // Ensure standard session key generator is used in production + if _, ok := conf.SessionKeyGenerator.(*sesskeygen.Default); !ok { + return errors.New( + "standard session key generator " + + "must be used in production mode", + ) + } + + // Ensure bcrypt password hasher is used in production + if _, ok := conf.PasswordHasher.(passhash.Bcrypt); !ok { + return errors.New( + "bcrypt password hasher must be used in production mode", + ) + } + } + + return conf.DebugUser.Prepares(conf.Mode) +} diff --git a/api/config/sessionKeyGenerator.go b/api/config/sessionKeyGenerator.go new file mode 100644 index 0000000..927fcac --- /dev/null +++ b/api/config/sessionKeyGenerator.go @@ -0,0 +1,26 @@ +package config + +import ( + "fmt" + "reflect" +) + +// SessionKeyGenerator represents a session key generator identifier +type SessionKeyGenerator string + +// UnmarshalTOML implements the TOML unmarshaler interface +func (v *SessionKeyGenerator) UnmarshalTOML(val interface{}) error { + if str, isString := val.(string); isString { + switch str { + case "default": + *v = SessionKeyGenerator(str) + default: + return fmt.Errorf("unknown session key generator: '%s'", val) + } + return nil + } + return fmt.Errorf( + "unexpected session key generator value type: %s", + reflect.TypeOf(val), + ) +} diff --git a/api/config/tlsCipherSuite.go b/api/config/tlsCipherSuite.go new file mode 100644 index 0000000..4fb8ed1 --- /dev/null +++ b/api/config/tlsCipherSuite.go @@ -0,0 +1,75 @@ +package config + +import ( + "crypto/tls" + "fmt" + "reflect" +) + +// TLSCipherSuite represents a TLS cipher suite +type TLSCipherSuite uint16 + +// UnmarshalTOML implements the TOML unmarshaler interface +func (v *TLSCipherSuite) UnmarshalTOML(val interface{}) error { + if str, isString := val.(string); isString { + switch str { + case "RSA_WITH_RC4_128_SHA": + *v = TLSCipherSuite(tls.TLS_RSA_WITH_RC4_128_SHA) + case "RSA_WITH_3DES_EDE_CBC_SHA": + *v = TLSCipherSuite(tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA) + case "RSA_WITH_AES_128_CBC_SHA": + *v = TLSCipherSuite(tls.TLS_RSA_WITH_AES_128_CBC_SHA) + case "RSA_WITH_AES_256_CBC_SHA": + *v = TLSCipherSuite(tls.TLS_RSA_WITH_AES_256_CBC_SHA) + case "RSA_WITH_AES_128_CBC_SHA256": + *v = TLSCipherSuite(tls.TLS_RSA_WITH_AES_128_CBC_SHA256) + case "RSA_WITH_AES_128_GCM_SHA256": + *v = TLSCipherSuite(tls.TLS_RSA_WITH_AES_128_GCM_SHA256) + case "RSA_WITH_AES_256_GCM_SHA384": + *v = TLSCipherSuite(tls.TLS_RSA_WITH_AES_256_GCM_SHA384) + case "ECDHE_ECDSA_WITH_RC4_128_SHA": + *v = TLSCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA) + case "ECDHE_ECDSA_WITH_AES_128_CBC_SHA": + *v = TLSCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA) + case "ECDHE_ECDSA_WITH_AES_256_CBC_SHA": + *v = TLSCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA) + case "ECDHE_RSA_WITH_RC4_128_SHA": + *v = TLSCipherSuite(tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA) + case "ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": + *v = TLSCipherSuite(tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA) + case "ECDHE_RSA_WITH_AES_128_CBC_SHA": + *v = TLSCipherSuite(tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) + case "ECDHE_RSA_WITH_AES_256_CBC_SHA": + *v = TLSCipherSuite(tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA) + case "ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": + *v = TLSCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256) + case "ECDHE_RSA_WITH_AES_128_CBC_SHA256": + *v = TLSCipherSuite(tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256) + case "ECDHE_RSA_WITH_AES_128_GCM_SHA256": + *v = TLSCipherSuite(tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) + case "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": + *v = TLSCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + case "ECDHE_RSA_WITH_AES_256_GCM_SHA384": + *v = TLSCipherSuite(tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) + case "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": + *v = TLSCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + case "ECDHE_RSA_WITH_CHACHA20_POLY1305": + *v = TLSCipherSuite(tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305) + case "ECDHE_ECDSA_WITH_CHACHA20_POLY1305": + *v = TLSCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305) + case "AES_128_GCM_SHA256": + *v = TLSCipherSuite(tls.TLS_AES_128_GCM_SHA256) + case "AES_256_GCM_SHA384": + *v = TLSCipherSuite(tls.TLS_AES_256_GCM_SHA384) + case "CHACHA20_POLY1305_SHA256": + *v = TLSCipherSuite(tls.TLS_CHACHA20_POLY1305_SHA256) + default: + return fmt.Errorf("unknown TLS cipher suite: '%s'", val) + } + return nil + } + return fmt.Errorf( + "unexpected TLS cipher suite value type: %s", + reflect.TypeOf(val), + ) +} diff --git a/api/config/tlsCurveID.go b/api/config/tlsCurveID.go new file mode 100644 index 0000000..c14749e --- /dev/null +++ b/api/config/tlsCurveID.go @@ -0,0 +1,33 @@ +package config + +import ( + "crypto/tls" + "fmt" + "reflect" +) + +// TLSCurveID represents a TLS curve identifier +type TLSCurveID tls.CurveID + +// UnmarshalTOML implements the TOML unmarshaler interface +func (v *TLSCurveID) UnmarshalTOML(val interface{}) error { + if str, isString := val.(string); isString { + switch str { + case "CurveP256": + *v = TLSCurveID(tls.CurveP256) + case "CurveP384": + *v = TLSCurveID(tls.CurveP384) + case "CurveP521": + *v = TLSCurveID(tls.CurveP521) + case "X25519": + *v = TLSCurveID(tls.X25519) + default: + return fmt.Errorf("unknown TLS curve ID: '%s'", val) + } + return nil + } + return fmt.Errorf( + "unexpected TLS curve value type: %s", + reflect.TypeOf(val), + ) +} diff --git a/api/config/tlsVersion.go b/api/config/tlsVersion.go new file mode 100644 index 0000000..7a6c749 --- /dev/null +++ b/api/config/tlsVersion.go @@ -0,0 +1,35 @@ +package config + +import ( + "crypto/tls" + "fmt" + "reflect" +) + +// TLSVersion represents a TLS protocol version +type TLSVersion uint16 + +// UnmarshalTOML implements the TOML unmarshaler interface +func (v *TLSVersion) UnmarshalTOML(val interface{}) error { + if str, isString := val.(string); isString { + switch str { + case "SSL 3.0": + *v = TLSVersion(tls.VersionSSL30) + case "TLS 1.0": + *v = TLSVersion(tls.VersionTLS10) + case "TLS 1.1": + *v = TLSVersion(tls.VersionTLS11) + case "TLS 1.2": + *v = TLSVersion(tls.VersionTLS12) + case "TLS 1.3": + *v = TLSVersion(tls.VersionTLS13) + default: + return fmt.Errorf("unknown TLS protocol version: '%s'", val) + } + return nil + } + return fmt.Errorf( + "unexpected TLS protocol version value type: %s", + reflect.TypeOf(val), + ) +} diff --git a/api/onDebugSess.go b/api/onDebugSess.go index 62c05d6..969a9fa 100644 --- a/api/onDebugSess.go +++ b/api/onDebugSess.go @@ -8,8 +8,8 @@ func (srv *server) onDebugSess( username, password string, ) []byte { // Check debug credentials - if username != srv.opts.DebugUser.Username || - password != srv.opts.DebugUser.Password { + if username != srv.conf.DebugUser.Username || + password != srv.conf.DebugUser.Password { return nil } diff --git a/api/options/options.go b/api/options/options.go deleted file mode 100644 index ed96c80..0000000 --- a/api/options/options.go +++ /dev/null @@ -1,193 +0,0 @@ -package options - -import ( - "errors" - "log" - "os" - - "github.com/romshark/dgraph_graphql_go/api/passhash" - "github.com/romshark/dgraph_graphql_go/api/sesskeygen" - "github.com/romshark/dgraph_graphql_go/api/transport" - thttp "github.com/romshark/dgraph_graphql_go/api/transport/http" -) - -// Mode defines the server mode -type Mode string - -const ( - // ModeDebug represents the debug server mode - ModeDebug Mode = "debug" - - // ModeBeta represents the beta server mode - ModeBeta Mode = "beta" - - // ModeProduction represents the production server mode - ModeProduction Mode = "production" -) - -// DebugUserStatus defines the debug user status option -type DebugUserStatus string - -const ( - // DebugUserUnset represents the default unset option value - DebugUserUnset DebugUserStatus = "" - - // DebugUserDisabled disables the debug user - DebugUserDisabled DebugUserStatus = "disabled" - - // DebugUserReadOnly enables the debug user in a read-only mode - DebugUserReadOnly DebugUserStatus = "read-only" - - // DebugUserRW enables the debug user in a read-write mode - DebugUserRW DebugUserStatus = "read-write" -) - -// DebugUserOptions defines the API debug user options -type DebugUserOptions struct { - Status DebugUserStatus - Username string - Password string -} - -// Prepares sets defaults and validates the options -func (opts *DebugUserOptions) Prepares(mode Mode) error { - // Set default debug user option - if opts.Status == DebugUserUnset { - switch mode { - case ModeProduction: - opts.Status = DebugUserDisabled - case ModeBeta: - opts.Status = DebugUserReadOnly - default: - opts.Status = DebugUserRW - } - } - - // Use "debug" as the default debug username - if opts.Username == "" { - opts.Username = "debug" - } - - // Use "debug" as the default debug password - if opts.Password == "" { - opts.Password = "debug" - } - - // VALIDATE - - // Ensure the debug user isn't enabled in production mode - if mode == ModeProduction { - if opts.Status != DebugUserDisabled { - return errors.New("debug user must be disabled in production mode") - } - } - - return nil -} - -// ServerOptions defines the API server options -type ServerOptions struct { - Mode Mode - Host string - DBHost string - SessionKeyGenerator sesskeygen.SessionKeyGenerator - PasswordHasher passhash.PasswordHasher - DebugUser DebugUserOptions - Transport []transport.Server - DebugLog *log.Logger - ErrorLog *log.Logger -} - -// Prepare sets defaults and validates the options -func (opts *ServerOptions) Prepare() error { - // Use production mode by default - if opts.Mode == "" { - opts.Mode = ModeProduction - } - - // Set default database host address - if opts.DBHost == "" { - switch opts.Mode { - case ModeProduction: - opts.DBHost = "localhost:9080" - default: - opts.DBHost = "localhost:10180" - } - } - - // Use default session key generator - if opts.SessionKeyGenerator == nil { - opts.SessionKeyGenerator = sesskeygen.NewDefault() - } - - // Use default password hasher - if opts.PasswordHasher == nil { - opts.PasswordHasher = passhash.Bcrypt{} - } - - // Use default debug logger to stdout - if opts.DebugLog == nil { - opts.DebugLog = log.New( - os.Stdout, - "DBG: ", - log.Ldate|log.Ltime, - ) - } - - // Use default error logger to stderr - if opts.ErrorLog == nil { - opts.ErrorLog = log.New( - os.Stderr, - "ERR: ", - log.Ldate|log.Ltime, - ) - } - - // VALIDATE - - // Ensure at least one transport adapter is specified - if len(opts.Transport) < 1 { - return errors.New("no transport adapter") - } - - if opts.Mode == ModeProduction { - // Validate transport adapters - for _, trn := range opts.Transport { - if httpAdapter, ok := trn.(*thttp.Server); ok { - // Ensure TLS is enabled in production on all transport adapters - opts := httpAdapter.Options() - if opts.TLS == nil { - return errors.New( - "TLS must not be disabled on HTTP transport adapter " + - "in production mode", - ) - } - - // Ensure playground is disabled in production - if opts.Playground { - return errors.New( - "the playground must be disabled on " + - "HTTP transport adapter in production mode", - ) - } - } - } - - // Ensure standard session key generator is used in production - if _, ok := opts.SessionKeyGenerator.(*sesskeygen.Default); !ok { - return errors.New( - "standard session key generator " + - "must be used in production mode", - ) - } - - // Ensure bcrypt password hasher is used in production - if _, ok := opts.PasswordHasher.(passhash.Bcrypt); !ok { - return errors.New( - "bcrypt password hasher must be used in production mode", - ) - } - } - - return opts.DebugUser.Prepares(opts.Mode) -} diff --git a/api/transport/http/client.go b/api/transport/http/client.go index 03600c5..d0f76ce 100644 --- a/api/transport/http/client.go +++ b/api/transport/http/client.go @@ -24,7 +24,7 @@ type Client struct { } // NewClient creates a new API client instance -func NewClient(host url.URL, opts ClientOptions) (trn.Client, error) { +func NewClient(host url.URL, conf ClientConfig) (trn.Client, error) { // Initialize the http cookie jar cookieJar, err := cookiejar.New(nil) if err != nil { @@ -35,7 +35,7 @@ func NewClient(host url.URL, opts ClientOptions) (trn.Client, error) { return &Client{ host: host, httpClt: &http.Client{ - Timeout: opts.Timeout, + Timeout: conf.Timeout, Jar: cookieJar, }, }, nil diff --git a/api/transport/http/options.go b/api/transport/http/conf.go similarity index 50% rename from api/transport/http/options.go rename to api/transport/http/conf.go index 0d8ce12..58d8a4b 100644 --- a/api/transport/http/options.go +++ b/api/transport/http/conf.go @@ -6,14 +6,14 @@ import ( "time" ) -// ServerTLS represents the TLS options +// ServerTLS represents the TLS configurations type ServerTLS struct { Config *tls.Config CertificateFilePath string PrivateKeyFilePath string } -// Clone creates an exact detached copy of the server TLS options +// Clone creates an exact detached copy of the server TLS configurations func (stls *ServerTLS) Clone() *ServerTLS { if stls == nil { return nil @@ -29,25 +29,25 @@ func (stls *ServerTLS) Clone() *ServerTLS { } } -// ServerOptions defines the HTTP server transport layer options -type ServerOptions struct { +// ServerConfig defines the HTTP server transport layer configurations +type ServerConfig struct { Host string KeepAliveDuration time.Duration TLS *ServerTLS Playground bool } -// Prepare sets defaults and validates the options -func (opts *ServerOptions) Prepare() error { - if opts.KeepAliveDuration == time.Duration(0) { - opts.KeepAliveDuration = 3 * time.Minute +// Prepare sets defaults and validates the configurations +func (conf *ServerConfig) Prepare() error { + if conf.KeepAliveDuration == time.Duration(0) { + conf.KeepAliveDuration = 3 * time.Minute } - if opts.TLS != nil { - if opts.TLS.CertificateFilePath == "" { + if conf.TLS != nil { + if conf.TLS.CertificateFilePath == "" { return errors.New("missing TLS certificate file path") } - if opts.TLS.PrivateKeyFilePath == "" { + if conf.TLS.PrivateKeyFilePath == "" { return errors.New("missing TLS private key file path") } } @@ -55,14 +55,14 @@ func (opts *ServerOptions) Prepare() error { return nil } -// ClientOptions defines the HTTP client transport layer options -type ClientOptions struct { +// ClientConfig defines the HTTP client transport layer configuration +type ClientConfig struct { Timeout time.Duration } -// SetDefaults sets the default options -func (opts *ClientOptions) SetDefaults() { - if opts.Timeout == time.Duration(0) { - opts.Timeout = 30 * time.Second +// SetDefaults sets the default configuration +func (conf *ClientConfig) SetDefaults() { + if conf.Timeout == time.Duration(0) { + conf.Timeout = 30 * time.Second } } diff --git a/api/transport/http/servePlayground.go b/api/transport/http/servePlayground.go index cec6a10..98a8515 100644 --- a/api/transport/http/servePlayground.go +++ b/api/transport/http/servePlayground.go @@ -9,7 +9,7 @@ func (t *Server) servePlayground( resp http.ResponseWriter, req *http.Request, ) { - if !t.opts.Playground { + if !t.conf.Playground { http.Error( resp, http.StatusText(http.StatusNotFound), diff --git a/api/transport/http/server.go b/api/transport/http/server.go index 6289777..7666da9 100644 --- a/api/transport/http/server.go +++ b/api/transport/http/server.go @@ -15,7 +15,7 @@ import ( // Server represents an HTTP based server transport implementation type Server struct { addrReadWait *sync.WaitGroup - opts ServerOptions + conf ServerConfig httpSrv *http.Server addr net.Addr onGraphQuery trn.OnGraphQuery @@ -28,22 +28,22 @@ type Server struct { // NewServer creates a new unencrypted JSON based HTTP transport. // Use NewSecure to enable encryption instead -func NewServer(opts ServerOptions) (trn.Server, error) { - if err := opts.Prepare(); err != nil { +func NewServer(conf ServerConfig) (trn.Server, error) { + if err := conf.Prepare(); err != nil { return nil, err } t := &Server{ addrReadWait: &sync.WaitGroup{}, - opts: opts, + conf: conf, } t.httpSrv = &http.Server{ - Addr: opts.Host, + Addr: conf.Host, Handler: t, } - if opts.TLS != nil { - t.httpSrv.TLSConfig = opts.TLS.Config + if conf.TLS != nil { + t.httpSrv.TLSConfig = conf.TLS.Config } t.addrReadWait.Add(1) @@ -97,16 +97,16 @@ func (t *Server) Run() error { tcpListener := tcpKeepAliveListener{ TCPListener: listener.(*net.TCPListener), - KeepAliveDuration: t.opts.KeepAliveDuration, + KeepAliveDuration: t.conf.KeepAliveDuration, } - if t.opts.TLS != nil { + if t.conf.TLS != nil { t.debugLog.Print("listening https://" + t.addr.String()) if err := t.httpSrv.ServeTLS( tcpListener, - t.opts.TLS.CertificateFilePath, - t.opts.TLS.PrivateKeyFilePath, + t.conf.TLS.CertificateFilePath, + t.conf.TLS.PrivateKeyFilePath, ); err != http.ErrServerClosed { return err } @@ -176,12 +176,12 @@ func (t *Server) Addr() url.URL { } } -// Options returns the active configuration -func (t *Server) Options() ServerOptions { - return ServerOptions{ - Host: t.opts.Host, - KeepAliveDuration: t.opts.KeepAliveDuration, - TLS: t.opts.TLS.Clone(), - Playground: t.opts.Playground, +// Config returns the active configuration +func (t *Server) Config() ServerConfig { + return ServerConfig{ + Host: t.conf.Host, + KeepAliveDuration: t.conf.KeepAliveDuration, + TLS: t.conf.TLS.Clone(), + Playground: t.conf.Playground, } } diff --git a/api/validator/email.go b/api/validator/email.go index 989790f..87f9f08 100644 --- a/api/validator/email.go +++ b/api/validator/email.go @@ -4,11 +4,11 @@ import "github.com/pkg/errors" // Email implements the Validator interface func (vld *validator) Email(v string) error { - if uint(len(v)) > vld.opts.EmailLenMax { + if uint(len(v)) > vld.conf.EmailLenMax { return errors.Errorf( "email address too long (%d / %d)", len(v), - vld.opts.EmailLenMax, + vld.conf.EmailLenMax, ) } if !vld.regexpEmail.MatchString(v) { diff --git a/api/validator/password.go b/api/validator/password.go index 4afa9e2..6e894b9 100644 --- a/api/validator/password.go +++ b/api/validator/password.go @@ -4,18 +4,18 @@ import "github.com/pkg/errors" // Password implements the Validator interface func (vld *validator) Password(v string) error { - if uint(len(v)) < vld.opts.PasswordLenMin { + if uint(len(v)) < vld.conf.PasswordLenMin { return errors.Errorf( "password too short (%d / %d)", len(v), - vld.opts.PasswordLenMin, + vld.conf.PasswordLenMin, ) } - if uint(len(v)) > vld.opts.PasswordLenMax { + if uint(len(v)) > vld.conf.PasswordLenMax { return errors.Errorf( "password too long (%d / %d)", len(v), - vld.opts.PasswordLenMax, + vld.conf.PasswordLenMax, ) } return nil diff --git a/api/validator/postContents.go b/api/validator/postContents.go index 21a5a05..cc1df5f 100644 --- a/api/validator/postContents.go +++ b/api/validator/postContents.go @@ -4,17 +4,17 @@ import "github.com/pkg/errors" // PostContents implements the Validator interface func (vld *validator) PostContents(v string) error { - if uint(len(v)) < vld.opts.PostContentsLenMin { + if uint(len(v)) < vld.conf.PostContentsLenMin { return errors.Errorf( "Post.contents too short (min: %d)", - vld.opts.PostContentsLenMin, + vld.conf.PostContentsLenMin, ) } - if uint(len(v)) > vld.opts.PostContentsLenMax { + if uint(len(v)) > vld.conf.PostContentsLenMax { return errors.Errorf( "Post.contents too long (%d / %d)", len(v), - vld.opts.PostContentsLenMax, + vld.conf.PostContentsLenMax, ) } return nil diff --git a/api/validator/postTitle.go b/api/validator/postTitle.go index 3ab81c7..c034b97 100644 --- a/api/validator/postTitle.go +++ b/api/validator/postTitle.go @@ -4,17 +4,17 @@ import "github.com/pkg/errors" // PostTitle implements the Valiator interface func (vld *validator) PostTitle(v string) error { - if uint(len(v)) < vld.opts.PostTitleLenMin { + if uint(len(v)) < vld.conf.PostTitleLenMin { return errors.Errorf( "Post.title too short (min: %d)", - vld.opts.PostTitleLenMin, + vld.conf.PostTitleLenMin, ) } - if uint(len(v)) > vld.opts.PostTitleLenMax { + if uint(len(v)) > vld.conf.PostTitleLenMax { return errors.Errorf( "Post.title too long (%d / %d)", len(v), - vld.opts.PostTitleLenMax, + vld.conf.PostTitleLenMax, ) } return nil diff --git a/api/validator/reactionMessage.go b/api/validator/reactionMessage.go index a9f6403..3620bf1 100644 --- a/api/validator/reactionMessage.go +++ b/api/validator/reactionMessage.go @@ -6,17 +6,17 @@ import ( // ReactionMessage implements the Validator interface func (vld *validator) ReactionMessage(v string) error { - if uint(len(v)) < vld.opts.ReactionMessageLenMin { + if uint(len(v)) < vld.conf.ReactionMessageLenMin { return errors.Errorf( "Reaction.message too short (min: %d)", - vld.opts.ReactionMessageLenMin, + vld.conf.ReactionMessageLenMin, ) } - if uint(len(v)) > vld.opts.ReactionMessageLenMax { + if uint(len(v)) > vld.conf.ReactionMessageLenMax { return errors.Errorf( "Reaction.message too long (%d / %d)", len(v), - vld.opts.ReactionMessageLenMax, + vld.conf.ReactionMessageLenMax, ) } return nil diff --git a/api/validator/userDisplayName.go b/api/validator/userDisplayName.go index 8f96672..346d5ab 100644 --- a/api/validator/userDisplayName.go +++ b/api/validator/userDisplayName.go @@ -4,17 +4,17 @@ import "github.com/pkg/errors" // UserDisplayName implements the Validator interface func (vld *validator) UserDisplayName(v string) error { - if uint(len(v)) < vld.opts.UserDisplayNameLenMin { + if uint(len(v)) < vld.conf.UserDisplayNameLenMin { return errors.Errorf( "User.displayName too short (min: %d)", - vld.opts.UserDisplayNameLenMin, + vld.conf.UserDisplayNameLenMin, ) } - if uint(len(v)) > vld.opts.UserDisplayNameLenMax { + if uint(len(v)) > vld.conf.UserDisplayNameLenMax { return errors.Errorf( "User.displayName too long (%d / %d)", len(v), - vld.opts.UserDisplayNameLenMax, + vld.conf.UserDisplayNameLenMax, ) } return nil diff --git a/api/validator/validator.go b/api/validator/validator.go index 66dbe46..cb5536f 100644 --- a/api/validator/validator.go +++ b/api/validator/validator.go @@ -34,8 +34,8 @@ type Validator interface { UserDisplayName(v string) error } -// Options represents the validator options -type Options struct { +// Config represents the validator configuration +type Config struct { PasswordLenMin uint PasswordLenMax uint EmailLenMax uint @@ -50,26 +50,26 @@ type Options struct { } type validator struct { - opts Options + conf Config regexpEmail *regexp.Regexp } // NewValidator creates a new validator instance -func NewValidator(productionModeEnabled bool, opts Options) (Validator, error) { +func NewValidator(productionModeEnabled bool, conf Config) (Validator, error) { regexpEmail, err := regexp.Compile(`^.+@.+\..+$`) if err != nil { panic(errors.Wrap(err, "compile regexpEmail")) } - if productionModeEnabled && opts.PasswordLenMin < 6 { + if productionModeEnabled && conf.PasswordLenMin < 6 { return nil, fmt.Errorf( "minimum password length must be 6 in production mode, was: %d", - opts.PasswordLenMin, + conf.PasswordLenMin, ) } return &validator{ - opts: opts, + conf: conf, regexpEmail: regexpEmail, }, nil } diff --git a/apitest/setup/client.go b/apitest/setup/client.go index 5fa34eb..72eaa7d 100644 --- a/apitest/setup/client.go +++ b/apitest/setup/client.go @@ -46,7 +46,7 @@ func (ts *TestSetup) Guest() *Client { Scheme: "http", Host: ts.serverTransport.(*thttp.Server).Addr().Host, }, - thttp.ClientOptions{ + thttp.ClientConfig{ Timeout: time.Second * 10, }, ) diff --git a/apitest/setup/setup.go b/apitest/setup/setup.go index 8ef4413..2e64614 100644 --- a/apitest/setup/setup.go +++ b/apitest/setup/setup.go @@ -9,7 +9,7 @@ import ( "github.com/dgraph-io/dgo" dbapi "github.com/dgraph-io/dgo/protos/api" "github.com/romshark/dgraph_graphql_go/api" - "github.com/romshark/dgraph_graphql_go/api/options" + "github.com/romshark/dgraph_graphql_go/api/config" trn "github.com/romshark/dgraph_graphql_go/api/transport" thttp "github.com/romshark/dgraph_graphql_go/api/transport/http" "github.com/stretchr/testify/require" @@ -55,18 +55,18 @@ func New(t *testing.T, context TestContext) *TestSetup { )) require.NoError(t, conn.Close()) - serverTransport, err := thttp.NewServer(thttp.ServerOptions{ + serverTransport, err := thttp.NewServer(thttp.ServerConfig{ Host: context.SrvHost, Playground: false, }) require.NoError(t, err) - srvOpts := options.ServerOptions{ - Mode: options.ModeDebug, + serverConfig := &config.ServerConfig{ + Mode: config.ModeDebug, DBHost: context.DBHost, - DebugUser: options.DebugUserOptions{ + DebugUser: config.DebugUserConfig{ // Enable the debug user in read-write mode - Status: options.DebugUserRW, + Mode: config.DebugUserRW, Username: debugUsername, Password: debugPassword, }, @@ -75,7 +75,7 @@ func New(t *testing.T, context TestContext) *TestSetup { }, } - apiServer, err := api.NewServer(srvOpts) + apiServer, err := api.NewServer(serverConfig) require.NoError(t, err) require.NoError(t, apiServer.Launch()) diff --git a/cmd/api/config.toml b/cmd/api/config.toml new file mode 100644 index 0000000..d10cce2 --- /dev/null +++ b/cmd/api/config.toml @@ -0,0 +1,39 @@ +# Go + GraphQL + Dgraph demo by github.com/romshark +# +# API server configuration + +mode = "debug" +password-hasher = "bcrypt" +session-key-generator = "default" + +[db] +host = "localhost:10180" + +[log] +debug = "stdout" +error = "stderr" + +[debug] +mode = "read-write" +username = "debug" +password = "debug" + +[transport-http] +host = "localhost:16000" +keep-alive = "3min" +playground = true + +[transport-http.tls] +enabled = true +min-version = "TLS 1.2" +curve-preferences = [ + "X25519", + "CurveP256" +] +cipher-suites = [ + "ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "AES_128_GCM_SHA256" +] +certificate-file = "./demo.crt" +key-file = "./demo.key" \ No newline at end of file diff --git a/cmd/api/file b/cmd/api/file new file mode 100644 index 0000000..e69de29 diff --git a/cmd/api/main.go b/cmd/api/main.go index d4f00b7..d43714d 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -2,77 +2,28 @@ package main import ( "context" - "crypto/tls" "flag" "log" "github.com/romshark/dgraph_graphql_go/api" - "github.com/romshark/dgraph_graphql_go/api/options" - "github.com/romshark/dgraph_graphql_go/api/passhash" - "github.com/romshark/dgraph_graphql_go/api/sesskeygen" - "github.com/romshark/dgraph_graphql_go/api/transport" - thttp "github.com/romshark/dgraph_graphql_go/api/transport/http" + "github.com/romshark/dgraph_graphql_go/api/config" ) -var host = flag.String("host", "localhost:16000", "API server host address") -var dbHost = flag.String("dbhost", "localhost:9080", "database host address") -var argCertFilePath = flag.String( - "tlscert", - "./demo.crt", - "path to the TLS certificate file", -) -var argPrivateKeyFile = flag.String( - "tlskey", - "./demo.key", - "path to the TLS private-key file", +var argConfigFile = flag.String( + "config", + "./config.toml", + "path to the configuration file", ) func main() { flag.Parse() - // Enable TLS if a certificate file is provided - var tlsConf *thttp.ServerTLS - if *argCertFilePath != "" { - tlsConf = &thttp.ServerTLS{ - Config: &tls.Config{ - MinVersion: tls.VersionTLS12, - CurvePreferences: []tls.CurveID{ - tls.X25519, - tls.CurveP256, - }, - PreferServerCipherSuites: true, - CipherSuites: []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_AES_128_GCM_SHA256, - }, - }, - CertificateFilePath: *argCertFilePath, - PrivateKeyFilePath: *argPrivateKeyFile, - } - } - - // Use HTTP as transport - transportHTTP, err := thttp.NewServer(thttp.ServerOptions{ - Host: *host, - TLS: tlsConf, - Playground: true, - }) + serverConfig, err := config.FromFile(*argConfigFile) if err != nil { - log.Fatalf("API server HTTP(S) transport init: %s", err) + log.Fatalf("reading config: %s", err) } - api, err := api.NewServer(options.ServerOptions{ - Mode: options.ModeBeta, - Host: *host, - DBHost: *dbHost, // database host address - SessionKeyGenerator: sesskeygen.NewDefault(), // session key generator - PasswordHasher: passhash.Bcrypt{}, // password hasher - Transport: []transport.Server{ - // HTTP(S) transport - transportHTTP, - }, - }) + api, err := api.NewServer(serverConfig) if err != nil { log.Fatalf("API server init: %s", err) } diff --git a/vendor/github.com/BurntSushi/toml/.gitignore b/vendor/github.com/BurntSushi/toml/.gitignore new file mode 100644 index 0000000..0cd3800 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/.gitignore @@ -0,0 +1,5 @@ +TAGS +tags +.*.swp +tomlcheck/tomlcheck +toml.test diff --git a/vendor/github.com/BurntSushi/toml/.travis.yml b/vendor/github.com/BurntSushi/toml/.travis.yml new file mode 100644 index 0000000..8b8afc4 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/.travis.yml @@ -0,0 +1,15 @@ +language: go +go: + - 1.1 + - 1.2 + - 1.3 + - 1.4 + - 1.5 + - 1.6 + - tip +install: + - go install ./... + - go get github.com/BurntSushi/toml-test +script: + - export PATH="$PATH:$HOME/gopath/bin" + - make test diff --git a/vendor/github.com/BurntSushi/toml/COMPATIBLE b/vendor/github.com/BurntSushi/toml/COMPATIBLE new file mode 100644 index 0000000..6efcfd0 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/COMPATIBLE @@ -0,0 +1,3 @@ +Compatible with TOML version +[v0.4.0](https://github.com/toml-lang/toml/blob/v0.4.0/versions/en/toml-v0.4.0.md) + diff --git a/vendor/github.com/BurntSushi/toml/COPYING b/vendor/github.com/BurntSushi/toml/COPYING new file mode 100644 index 0000000..01b5743 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 TOML authors + +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. diff --git a/vendor/github.com/BurntSushi/toml/Makefile b/vendor/github.com/BurntSushi/toml/Makefile new file mode 100644 index 0000000..3600848 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/Makefile @@ -0,0 +1,19 @@ +install: + go install ./... + +test: install + go test -v + toml-test toml-test-decoder + toml-test -encoder toml-test-encoder + +fmt: + gofmt -w *.go */*.go + colcheck *.go */*.go + +tags: + find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS + +push: + git push origin master + git push github master + diff --git a/vendor/github.com/BurntSushi/toml/README.md b/vendor/github.com/BurntSushi/toml/README.md new file mode 100644 index 0000000..7c1b37e --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/README.md @@ -0,0 +1,218 @@ +## TOML parser and encoder for Go with reflection + +TOML stands for Tom's Obvious, Minimal Language. This Go package provides a +reflection interface similar to Go's standard library `json` and `xml` +packages. This package also supports the `encoding.TextUnmarshaler` and +`encoding.TextMarshaler` interfaces so that you can define custom data +representations. (There is an example of this below.) + +Spec: https://github.com/toml-lang/toml + +Compatible with TOML version +[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md) + +Documentation: https://godoc.org/github.com/BurntSushi/toml + +Installation: + +```bash +go get github.com/BurntSushi/toml +``` + +Try the toml validator: + +```bash +go get github.com/BurntSushi/toml/cmd/tomlv +tomlv some-toml-file.toml +``` + +[![Build Status](https://travis-ci.org/BurntSushi/toml.svg?branch=master)](https://travis-ci.org/BurntSushi/toml) [![GoDoc](https://godoc.org/github.com/BurntSushi/toml?status.svg)](https://godoc.org/github.com/BurntSushi/toml) + +### Testing + +This package passes all tests in +[toml-test](https://github.com/BurntSushi/toml-test) for both the decoder +and the encoder. + +### Examples + +This package works similarly to how the Go standard library handles `XML` +and `JSON`. Namely, data is loaded into Go values via reflection. + +For the simplest example, consider some TOML file as just a list of keys +and values: + +```toml +Age = 25 +Cats = [ "Cauchy", "Plato" ] +Pi = 3.14 +Perfection = [ 6, 28, 496, 8128 ] +DOB = 1987-07-05T05:45:00Z +``` + +Which could be defined in Go as: + +```go +type Config struct { + Age int + Cats []string + Pi float64 + Perfection []int + DOB time.Time // requires `import time` +} +``` + +And then decoded with: + +```go +var conf Config +if _, err := toml.Decode(tomlData, &conf); err != nil { + // handle error +} +``` + +You can also use struct tags if your struct field name doesn't map to a TOML +key value directly: + +```toml +some_key_NAME = "wat" +``` + +```go +type TOML struct { + ObscureKey string `toml:"some_key_NAME"` +} +``` + +### Using the `encoding.TextUnmarshaler` interface + +Here's an example that automatically parses duration strings into +`time.Duration` values: + +```toml +[[song]] +name = "Thunder Road" +duration = "4m49s" + +[[song]] +name = "Stairway to Heaven" +duration = "8m03s" +``` + +Which can be decoded with: + +```go +type song struct { + Name string + Duration duration +} +type songs struct { + Song []song +} +var favorites songs +if _, err := toml.Decode(blob, &favorites); err != nil { + log.Fatal(err) +} + +for _, s := range favorites.Song { + fmt.Printf("%s (%s)\n", s.Name, s.Duration) +} +``` + +And you'll also need a `duration` type that satisfies the +`encoding.TextUnmarshaler` interface: + +```go +type duration struct { + time.Duration +} + +func (d *duration) UnmarshalText(text []byte) error { + var err error + d.Duration, err = time.ParseDuration(string(text)) + return err +} +``` + +### More complex usage + +Here's an example of how to load the example from the official spec page: + +```toml +# This is a TOML document. Boom. + +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +organization = "GitHub" +bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." +dob = 1979-05-27T07:32:00Z # 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] ] # just an update to make sure parsers support it + +# Line breaks are OK when inside arrays +hosts = [ + "alpha", + "omega" +] +``` + +And the corresponding Go types are: + +```go +type tomlConfig struct { + Title string + Owner ownerInfo + DB database `toml:"database"` + Servers map[string]server + Clients clients +} + +type ownerInfo struct { + Name string + Org string `toml:"organization"` + Bio string + DOB time.Time +} + +type database struct { + Server string + Ports []int + ConnMax int `toml:"connection_max"` + Enabled bool +} + +type server struct { + IP string + DC string +} + +type clients struct { + Data [][]interface{} + Hosts []string +} +``` + +Note that a case insensitive match will be tried if an exact match can't be +found. + +A working example of the above can be found in `_examples/example.{go,toml}`. diff --git a/vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/COPYING b/vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/COPYING new file mode 100644 index 0000000..01b5743 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 TOML authors + +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. diff --git a/vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/COPYING b/vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/COPYING new file mode 100644 index 0000000..01b5743 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 TOML authors + +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. diff --git a/vendor/github.com/BurntSushi/toml/cmd/tomlv/COPYING b/vendor/github.com/BurntSushi/toml/cmd/tomlv/COPYING new file mode 100644 index 0000000..01b5743 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/cmd/tomlv/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 TOML authors + +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. diff --git a/vendor/github.com/BurntSushi/toml/decode.go b/vendor/github.com/BurntSushi/toml/decode.go new file mode 100644 index 0000000..b0fd51d --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/decode.go @@ -0,0 +1,509 @@ +package toml + +import ( + "fmt" + "io" + "io/ioutil" + "math" + "reflect" + "strings" + "time" +) + +func e(format string, args ...interface{}) error { + return fmt.Errorf("toml: "+format, args...) +} + +// Unmarshaler is the interface implemented by objects that can unmarshal a +// TOML description of themselves. +type Unmarshaler interface { + UnmarshalTOML(interface{}) error +} + +// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`. +func Unmarshal(p []byte, v interface{}) error { + _, err := Decode(string(p), v) + return err +} + +// Primitive is a TOML value that hasn't been decoded into a Go value. +// When using the various `Decode*` functions, the type `Primitive` may +// be given to any value, and its decoding will be delayed. +// +// A `Primitive` value can be decoded using the `PrimitiveDecode` function. +// +// The underlying representation of a `Primitive` value is subject to change. +// Do not rely on it. +// +// N.B. Primitive values are still parsed, so using them will only avoid +// the overhead of reflection. They can be useful when you don't know the +// exact type of TOML data until run time. +type Primitive struct { + undecoded interface{} + context Key +} + +// DEPRECATED! +// +// Use MetaData.PrimitiveDecode instead. +func PrimitiveDecode(primValue Primitive, v interface{}) error { + md := MetaData{decoded: make(map[string]bool)} + return md.unify(primValue.undecoded, rvalue(v)) +} + +// PrimitiveDecode is just like the other `Decode*` functions, except it +// decodes a TOML value that has already been parsed. Valid primitive values +// can *only* be obtained from values filled by the decoder functions, +// including this method. (i.e., `v` may contain more `Primitive` +// values.) +// +// Meta data for primitive values is included in the meta data returned by +// the `Decode*` functions with one exception: keys returned by the Undecoded +// method will only reflect keys that were decoded. Namely, any keys hidden +// behind a Primitive will be considered undecoded. Executing this method will +// update the undecoded keys in the meta data. (See the example.) +func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error { + md.context = primValue.context + defer func() { md.context = nil }() + return md.unify(primValue.undecoded, rvalue(v)) +} + +// Decode will decode the contents of `data` in TOML format into a pointer +// `v`. +// +// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be +// used interchangeably.) +// +// TOML arrays of tables correspond to either a slice of structs or a slice +// of maps. +// +// TOML datetimes correspond to Go `time.Time` values. +// +// All other TOML types (float, string, int, bool and array) correspond +// to the obvious Go types. +// +// An exception to the above rules is if a type implements the +// encoding.TextUnmarshaler interface. In this case, any primitive TOML value +// (floats, strings, integers, booleans and datetimes) will be converted to +// a byte string and given to the value's UnmarshalText method. See the +// Unmarshaler example for a demonstration with time duration strings. +// +// Key mapping +// +// TOML keys can map to either keys in a Go map or field names in a Go +// struct. The special `toml` struct tag may be used to map TOML keys to +// struct fields that don't match the key name exactly. (See the example.) +// A case insensitive match to struct names will be tried if an exact match +// can't be found. +// +// The mapping between TOML values and Go values is loose. That is, there +// may exist TOML values that cannot be placed into your representation, and +// there may be parts of your representation that do not correspond to +// TOML values. This loose mapping can be made stricter by using the IsDefined +// and/or Undecoded methods on the MetaData returned. +// +// This decoder will not handle cyclic types. If a cyclic type is passed, +// `Decode` will not terminate. +func Decode(data string, v interface{}) (MetaData, error) { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr { + return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v)) + } + if rv.IsNil() { + return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v)) + } + p, err := parse(data) + if err != nil { + return MetaData{}, err + } + md := MetaData{ + p.mapping, p.types, p.ordered, + make(map[string]bool, len(p.ordered)), nil, + } + return md, md.unify(p.mapping, indirect(rv)) +} + +// DecodeFile is just like Decode, except it will automatically read the +// contents of the file at `fpath` and decode it for you. +func DecodeFile(fpath string, v interface{}) (MetaData, error) { + bs, err := ioutil.ReadFile(fpath) + if err != nil { + return MetaData{}, err + } + return Decode(string(bs), v) +} + +// DecodeReader is just like Decode, except it will consume all bytes +// from the reader and decode it for you. +func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { + bs, err := ioutil.ReadAll(r) + if err != nil { + return MetaData{}, err + } + return Decode(string(bs), v) +} + +// unify performs a sort of type unification based on the structure of `rv`, +// which is the client representation. +// +// Any type mismatch produces an error. Finding a type that we don't know +// how to handle produces an unsupported type error. +func (md *MetaData) unify(data interface{}, rv reflect.Value) error { + + // Special case. Look for a `Primitive` value. + if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() { + // Save the undecoded data and the key context into the primitive + // value. + context := make(Key, len(md.context)) + copy(context, md.context) + rv.Set(reflect.ValueOf(Primitive{ + undecoded: data, + context: context, + })) + return nil + } + + // Special case. Unmarshaler Interface support. + if rv.CanAddr() { + if v, ok := rv.Addr().Interface().(Unmarshaler); ok { + return v.UnmarshalTOML(data) + } + } + + // Special case. Handle time.Time values specifically. + // TODO: Remove this code when we decide to drop support for Go 1.1. + // This isn't necessary in Go 1.2 because time.Time satisfies the encoding + // interfaces. + if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) { + return md.unifyDatetime(data, rv) + } + + // Special case. Look for a value satisfying the TextUnmarshaler interface. + if v, ok := rv.Interface().(TextUnmarshaler); ok { + return md.unifyText(data, v) + } + // BUG(burntsushi) + // The behavior here is incorrect whenever a Go type satisfies the + // encoding.TextUnmarshaler interface but also corresponds to a TOML + // hash or array. In particular, the unmarshaler should only be applied + // to primitive TOML values. But at this point, it will be applied to + // all kinds of values and produce an incorrect error whenever those values + // are hashes or arrays (including arrays of tables). + + k := rv.Kind() + + // laziness + if k >= reflect.Int && k <= reflect.Uint64 { + return md.unifyInt(data, rv) + } + switch k { + case reflect.Ptr: + elem := reflect.New(rv.Type().Elem()) + err := md.unify(data, reflect.Indirect(elem)) + if err != nil { + return err + } + rv.Set(elem) + return nil + case reflect.Struct: + return md.unifyStruct(data, rv) + case reflect.Map: + return md.unifyMap(data, rv) + case reflect.Array: + return md.unifyArray(data, rv) + case reflect.Slice: + return md.unifySlice(data, rv) + case reflect.String: + return md.unifyString(data, rv) + case reflect.Bool: + return md.unifyBool(data, rv) + case reflect.Interface: + // we only support empty interfaces. + if rv.NumMethod() > 0 { + return e("unsupported type %s", rv.Type()) + } + return md.unifyAnything(data, rv) + case reflect.Float32: + fallthrough + case reflect.Float64: + return md.unifyFloat64(data, rv) + } + return e("unsupported type %s", rv.Kind()) +} + +func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error { + tmap, ok := mapping.(map[string]interface{}) + if !ok { + if mapping == nil { + return nil + } + return e("type mismatch for %s: expected table but found %T", + rv.Type().String(), mapping) + } + + for key, datum := range tmap { + var f *field + fields := cachedTypeFields(rv.Type()) + for i := range fields { + ff := &fields[i] + if ff.name == key { + f = ff + break + } + if f == nil && strings.EqualFold(ff.name, key) { + f = ff + } + } + if f != nil { + subv := rv + for _, i := range f.index { + subv = indirect(subv.Field(i)) + } + if isUnifiable(subv) { + md.decoded[md.context.add(key).String()] = true + md.context = append(md.context, key) + if err := md.unify(datum, subv); err != nil { + return err + } + md.context = md.context[0 : len(md.context)-1] + } else if f.name != "" { + // Bad user! No soup for you! + return e("cannot write unexported field %s.%s", + rv.Type().String(), f.name) + } + } + } + return nil +} + +func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error { + tmap, ok := mapping.(map[string]interface{}) + if !ok { + if tmap == nil { + return nil + } + return badtype("map", mapping) + } + if rv.IsNil() { + rv.Set(reflect.MakeMap(rv.Type())) + } + for k, v := range tmap { + md.decoded[md.context.add(k).String()] = true + md.context = append(md.context, k) + + rvkey := indirect(reflect.New(rv.Type().Key())) + rvval := reflect.Indirect(reflect.New(rv.Type().Elem())) + if err := md.unify(v, rvval); err != nil { + return err + } + md.context = md.context[0 : len(md.context)-1] + + rvkey.SetString(k) + rv.SetMapIndex(rvkey, rvval) + } + return nil +} + +func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error { + datav := reflect.ValueOf(data) + if datav.Kind() != reflect.Slice { + if !datav.IsValid() { + return nil + } + return badtype("slice", data) + } + sliceLen := datav.Len() + if sliceLen != rv.Len() { + return e("expected array length %d; got TOML array of length %d", + rv.Len(), sliceLen) + } + return md.unifySliceArray(datav, rv) +} + +func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error { + datav := reflect.ValueOf(data) + if datav.Kind() != reflect.Slice { + if !datav.IsValid() { + return nil + } + return badtype("slice", data) + } + n := datav.Len() + if rv.IsNil() || rv.Cap() < n { + rv.Set(reflect.MakeSlice(rv.Type(), n, n)) + } + rv.SetLen(n) + return md.unifySliceArray(datav, rv) +} + +func (md *MetaData) unifySliceArray(data, rv reflect.Value) error { + sliceLen := data.Len() + for i := 0; i < sliceLen; i++ { + v := data.Index(i).Interface() + sliceval := indirect(rv.Index(i)) + if err := md.unify(v, sliceval); err != nil { + return err + } + } + return nil +} + +func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error { + if _, ok := data.(time.Time); ok { + rv.Set(reflect.ValueOf(data)) + return nil + } + return badtype("time.Time", data) +} + +func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error { + if s, ok := data.(string); ok { + rv.SetString(s) + return nil + } + return badtype("string", data) +} + +func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error { + if num, ok := data.(float64); ok { + switch rv.Kind() { + case reflect.Float32: + fallthrough + case reflect.Float64: + rv.SetFloat(num) + default: + panic("bug") + } + return nil + } + return badtype("float", data) +} + +func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error { + if num, ok := data.(int64); ok { + if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 { + switch rv.Kind() { + case reflect.Int, reflect.Int64: + // No bounds checking necessary. + case reflect.Int8: + if num < math.MinInt8 || num > math.MaxInt8 { + return e("value %d is out of range for int8", num) + } + case reflect.Int16: + if num < math.MinInt16 || num > math.MaxInt16 { + return e("value %d is out of range for int16", num) + } + case reflect.Int32: + if num < math.MinInt32 || num > math.MaxInt32 { + return e("value %d is out of range for int32", num) + } + } + rv.SetInt(num) + } else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 { + unum := uint64(num) + switch rv.Kind() { + case reflect.Uint, reflect.Uint64: + // No bounds checking necessary. + case reflect.Uint8: + if num < 0 || unum > math.MaxUint8 { + return e("value %d is out of range for uint8", num) + } + case reflect.Uint16: + if num < 0 || unum > math.MaxUint16 { + return e("value %d is out of range for uint16", num) + } + case reflect.Uint32: + if num < 0 || unum > math.MaxUint32 { + return e("value %d is out of range for uint32", num) + } + } + rv.SetUint(unum) + } else { + panic("unreachable") + } + return nil + } + return badtype("integer", data) +} + +func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error { + if b, ok := data.(bool); ok { + rv.SetBool(b) + return nil + } + return badtype("boolean", data) +} + +func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error { + rv.Set(reflect.ValueOf(data)) + return nil +} + +func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error { + var s string + switch sdata := data.(type) { + case TextMarshaler: + text, err := sdata.MarshalText() + if err != nil { + return err + } + s = string(text) + case fmt.Stringer: + s = sdata.String() + case string: + s = sdata + case bool: + s = fmt.Sprintf("%v", sdata) + case int64: + s = fmt.Sprintf("%d", sdata) + case float64: + s = fmt.Sprintf("%f", sdata) + default: + return badtype("primitive (string-like)", data) + } + if err := v.UnmarshalText([]byte(s)); err != nil { + return err + } + return nil +} + +// rvalue returns a reflect.Value of `v`. All pointers are resolved. +func rvalue(v interface{}) reflect.Value { + return indirect(reflect.ValueOf(v)) +} + +// indirect returns the value pointed to by a pointer. +// Pointers are followed until the value is not a pointer. +// New values are allocated for each nil pointer. +// +// An exception to this rule is if the value satisfies an interface of +// interest to us (like encoding.TextUnmarshaler). +func indirect(v reflect.Value) reflect.Value { + if v.Kind() != reflect.Ptr { + if v.CanSet() { + pv := v.Addr() + if _, ok := pv.Interface().(TextUnmarshaler); ok { + return pv + } + } + return v + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + return indirect(reflect.Indirect(v)) +} + +func isUnifiable(rv reflect.Value) bool { + if rv.CanSet() { + return true + } + if _, ok := rv.Interface().(TextUnmarshaler); ok { + return true + } + return false +} + +func badtype(expected string, data interface{}) error { + return e("cannot load TOML value of type %T into a Go %s", data, expected) +} diff --git a/vendor/github.com/BurntSushi/toml/decode_meta.go b/vendor/github.com/BurntSushi/toml/decode_meta.go new file mode 100644 index 0000000..b9914a6 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/decode_meta.go @@ -0,0 +1,121 @@ +package toml + +import "strings" + +// MetaData allows access to meta information about TOML data that may not +// be inferrable via reflection. In particular, whether a key has been defined +// and the TOML type of a key. +type MetaData struct { + mapping map[string]interface{} + types map[string]tomlType + keys []Key + decoded map[string]bool + context Key // Used only during decoding. +} + +// IsDefined returns true if the key given exists in the TOML data. The key +// should be specified hierarchially. e.g., +// +// // access the TOML key 'a.b.c' +// IsDefined("a", "b", "c") +// +// IsDefined will return false if an empty key given. Keys are case sensitive. +func (md *MetaData) IsDefined(key ...string) bool { + if len(key) == 0 { + return false + } + + var hash map[string]interface{} + var ok bool + var hashOrVal interface{} = md.mapping + for _, k := range key { + if hash, ok = hashOrVal.(map[string]interface{}); !ok { + return false + } + if hashOrVal, ok = hash[k]; !ok { + return false + } + } + return true +} + +// Type returns a string representation of the type of the key specified. +// +// Type will return the empty string if given an empty key or a key that +// does not exist. Keys are case sensitive. +func (md *MetaData) Type(key ...string) string { + fullkey := strings.Join(key, ".") + if typ, ok := md.types[fullkey]; ok { + return typ.typeString() + } + return "" +} + +// Key is the type of any TOML key, including key groups. Use (MetaData).Keys +// to get values of this type. +type Key []string + +func (k Key) String() string { + return strings.Join(k, ".") +} + +func (k Key) maybeQuotedAll() string { + var ss []string + for i := range k { + ss = append(ss, k.maybeQuoted(i)) + } + return strings.Join(ss, ".") +} + +func (k Key) maybeQuoted(i int) string { + quote := false + for _, c := range k[i] { + if !isBareKeyChar(c) { + quote = true + break + } + } + if quote { + return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\"" + } + return k[i] +} + +func (k Key) add(piece string) Key { + newKey := make(Key, len(k)+1) + copy(newKey, k) + newKey[len(k)] = piece + return newKey +} + +// Keys returns a slice of every key in the TOML data, including key groups. +// Each key is itself a slice, where the first element is the top of the +// hierarchy and the last is the most specific. +// +// The list will have the same order as the keys appeared in the TOML data. +// +// All keys returned are non-empty. +func (md *MetaData) Keys() []Key { + return md.keys +} + +// Undecoded returns all keys that have not been decoded in the order in which +// they appear in the original TOML document. +// +// This includes keys that haven't been decoded because of a Primitive value. +// Once the Primitive value is decoded, the keys will be considered decoded. +// +// Also note that decoding into an empty interface will result in no decoding, +// and so no keys will be considered decoded. +// +// In this sense, the Undecoded keys correspond to keys in the TOML document +// that do not have a concrete type in your representation. +func (md *MetaData) Undecoded() []Key { + undecoded := make([]Key, 0, len(md.keys)) + for _, key := range md.keys { + if !md.decoded[key.String()] { + undecoded = append(undecoded, key) + } + } + return undecoded +} diff --git a/vendor/github.com/BurntSushi/toml/doc.go b/vendor/github.com/BurntSushi/toml/doc.go new file mode 100644 index 0000000..b371f39 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/doc.go @@ -0,0 +1,27 @@ +/* +Package toml provides facilities for decoding and encoding TOML configuration +files via reflection. There is also support for delaying decoding with +the Primitive type, and querying the set of keys in a TOML document with the +MetaData type. + +The specification implemented: https://github.com/toml-lang/toml + +The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify +whether a file is a valid TOML document. It can also be used to print the +type of each key in a TOML document. + +Testing + +There are two important types of tests used for this package. The first is +contained inside '*_test.go' files and uses the standard Go unit testing +framework. These tests are primarily devoted to holistically testing the +decoder and encoder. + +The second type of testing is used to verify the implementation's adherence +to the TOML specification. These tests have been factored into their own +project: https://github.com/BurntSushi/toml-test + +The reason the tests are in a separate project is so that they can be used by +any implementation of TOML. Namely, it is language agnostic. +*/ +package toml diff --git a/vendor/github.com/BurntSushi/toml/encode.go b/vendor/github.com/BurntSushi/toml/encode.go new file mode 100644 index 0000000..d905c21 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/encode.go @@ -0,0 +1,568 @@ +package toml + +import ( + "bufio" + "errors" + "fmt" + "io" + "reflect" + "sort" + "strconv" + "strings" + "time" +) + +type tomlEncodeError struct{ error } + +var ( + errArrayMixedElementTypes = errors.New( + "toml: cannot encode array with mixed element types") + errArrayNilElement = errors.New( + "toml: cannot encode array with nil element") + errNonString = errors.New( + "toml: cannot encode a map with non-string key type") + errAnonNonStruct = errors.New( + "toml: cannot encode an anonymous field that is not a struct") + errArrayNoTable = errors.New( + "toml: TOML array element cannot contain a table") + errNoKey = errors.New( + "toml: top-level values must be Go maps or structs") + errAnything = errors.New("") // used in testing +) + +var quotedReplacer = strings.NewReplacer( + "\t", "\\t", + "\n", "\\n", + "\r", "\\r", + "\"", "\\\"", + "\\", "\\\\", +) + +// Encoder controls the encoding of Go values to a TOML document to some +// io.Writer. +// +// The indentation level can be controlled with the Indent field. +type Encoder struct { + // A single indentation level. By default it is two spaces. + Indent string + + // hasWritten is whether we have written any output to w yet. + hasWritten bool + w *bufio.Writer +} + +// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer +// given. By default, a single indentation level is 2 spaces. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{ + w: bufio.NewWriter(w), + Indent: " ", + } +} + +// Encode writes a TOML representation of the Go value to the underlying +// io.Writer. If the value given cannot be encoded to a valid TOML document, +// then an error is returned. +// +// The mapping between Go values and TOML values should be precisely the same +// as for the Decode* functions. Similarly, the TextMarshaler interface is +// supported by encoding the resulting bytes as strings. (If you want to write +// arbitrary binary data then you will need to use something like base64 since +// TOML does not have any binary types.) +// +// When encoding TOML hashes (i.e., Go maps or structs), keys without any +// sub-hashes are encoded first. +// +// If a Go map is encoded, then its keys are sorted alphabetically for +// deterministic output. More control over this behavior may be provided if +// there is demand for it. +// +// Encoding Go values without a corresponding TOML representation---like map +// types with non-string keys---will cause an error to be returned. Similarly +// for mixed arrays/slices, arrays/slices with nil elements, embedded +// non-struct types and nested slices containing maps or structs. +// (e.g., [][]map[string]string is not allowed but []map[string]string is OK +// and so is []map[string][]string.) +func (enc *Encoder) Encode(v interface{}) error { + rv := eindirect(reflect.ValueOf(v)) + if err := enc.safeEncode(Key([]string{}), rv); err != nil { + return err + } + return enc.w.Flush() +} + +func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) { + defer func() { + if r := recover(); r != nil { + if terr, ok := r.(tomlEncodeError); ok { + err = terr.error + return + } + panic(r) + } + }() + enc.encode(key, rv) + return nil +} + +func (enc *Encoder) encode(key Key, rv reflect.Value) { + // Special case. Time needs to be in ISO8601 format. + // Special case. If we can marshal the type to text, then we used that. + // Basically, this prevents the encoder for handling these types as + // generic structs (or whatever the underlying type of a TextMarshaler is). + switch rv.Interface().(type) { + case time.Time, TextMarshaler: + enc.keyEqElement(key, rv) + return + } + + k := rv.Kind() + switch k { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64, + reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: + enc.keyEqElement(key, rv) + case reflect.Array, reflect.Slice: + if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) { + enc.eArrayOfTables(key, rv) + } else { + enc.keyEqElement(key, rv) + } + case reflect.Interface: + if rv.IsNil() { + return + } + enc.encode(key, rv.Elem()) + case reflect.Map: + if rv.IsNil() { + return + } + enc.eTable(key, rv) + case reflect.Ptr: + if rv.IsNil() { + return + } + enc.encode(key, rv.Elem()) + case reflect.Struct: + enc.eTable(key, rv) + default: + panic(e("unsupported type for key '%s': %s", key, k)) + } +} + +// eElement encodes any value that can be an array element (primitives and +// arrays). +func (enc *Encoder) eElement(rv reflect.Value) { + switch v := rv.Interface().(type) { + case time.Time: + // Special case time.Time as a primitive. Has to come before + // TextMarshaler below because time.Time implements + // encoding.TextMarshaler, but we need to always use UTC. + enc.wf(v.UTC().Format("2006-01-02T15:04:05Z")) + return + case TextMarshaler: + // Special case. Use text marshaler if it's available for this value. + if s, err := v.MarshalText(); err != nil { + encPanic(err) + } else { + enc.writeQuoted(string(s)) + } + return + } + switch rv.Kind() { + case reflect.Bool: + enc.wf(strconv.FormatBool(rv.Bool())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64: + enc.wf(strconv.FormatInt(rv.Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64: + enc.wf(strconv.FormatUint(rv.Uint(), 10)) + case reflect.Float32: + enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32))) + case reflect.Float64: + enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64))) + case reflect.Array, reflect.Slice: + enc.eArrayOrSliceElement(rv) + case reflect.Interface: + enc.eElement(rv.Elem()) + case reflect.String: + enc.writeQuoted(rv.String()) + default: + panic(e("unexpected primitive type: %s", rv.Kind())) + } +} + +// By the TOML spec, all floats must have a decimal with at least one +// number on either side. +func floatAddDecimal(fstr string) string { + if !strings.Contains(fstr, ".") { + return fstr + ".0" + } + return fstr +} + +func (enc *Encoder) writeQuoted(s string) { + enc.wf("\"%s\"", quotedReplacer.Replace(s)) +} + +func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) { + length := rv.Len() + enc.wf("[") + for i := 0; i < length; i++ { + elem := rv.Index(i) + enc.eElement(elem) + if i != length-1 { + enc.wf(", ") + } + } + enc.wf("]") +} + +func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) { + if len(key) == 0 { + encPanic(errNoKey) + } + for i := 0; i < rv.Len(); i++ { + trv := rv.Index(i) + if isNil(trv) { + continue + } + panicIfInvalidKey(key) + enc.newline() + enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll()) + enc.newline() + enc.eMapOrStruct(key, trv) + } +} + +func (enc *Encoder) eTable(key Key, rv reflect.Value) { + panicIfInvalidKey(key) + if len(key) == 1 { + // Output an extra newline between top-level tables. + // (The newline isn't written if nothing else has been written though.) + enc.newline() + } + if len(key) > 0 { + enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll()) + enc.newline() + } + enc.eMapOrStruct(key, rv) +} + +func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) { + switch rv := eindirect(rv); rv.Kind() { + case reflect.Map: + enc.eMap(key, rv) + case reflect.Struct: + enc.eStruct(key, rv) + default: + panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String()) + } +} + +func (enc *Encoder) eMap(key Key, rv reflect.Value) { + rt := rv.Type() + if rt.Key().Kind() != reflect.String { + encPanic(errNonString) + } + + // Sort keys so that we have deterministic output. And write keys directly + // underneath this key first, before writing sub-structs or sub-maps. + var mapKeysDirect, mapKeysSub []string + for _, mapKey := range rv.MapKeys() { + k := mapKey.String() + if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) { + mapKeysSub = append(mapKeysSub, k) + } else { + mapKeysDirect = append(mapKeysDirect, k) + } + } + + var writeMapKeys = func(mapKeys []string) { + sort.Strings(mapKeys) + for _, mapKey := range mapKeys { + mrv := rv.MapIndex(reflect.ValueOf(mapKey)) + if isNil(mrv) { + // Don't write anything for nil fields. + continue + } + enc.encode(key.add(mapKey), mrv) + } + } + writeMapKeys(mapKeysDirect) + writeMapKeys(mapKeysSub) +} + +func (enc *Encoder) eStruct(key Key, rv reflect.Value) { + // Write keys for fields directly under this key first, because if we write + // a field that creates a new table, then all keys under it will be in that + // table (not the one we're writing here). + rt := rv.Type() + var fieldsDirect, fieldsSub [][]int + var addFields func(rt reflect.Type, rv reflect.Value, start []int) + addFields = func(rt reflect.Type, rv reflect.Value, start []int) { + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i) + // skip unexported fields + if f.PkgPath != "" && !f.Anonymous { + continue + } + frv := rv.Field(i) + if f.Anonymous { + t := f.Type + switch t.Kind() { + case reflect.Struct: + // Treat anonymous struct fields with + // tag names as though they are not + // anonymous, like encoding/json does. + if getOptions(f.Tag).name == "" { + addFields(t, frv, f.Index) + continue + } + case reflect.Ptr: + if t.Elem().Kind() == reflect.Struct && + getOptions(f.Tag).name == "" { + if !frv.IsNil() { + addFields(t.Elem(), frv.Elem(), f.Index) + } + continue + } + // Fall through to the normal field encoding logic below + // for non-struct anonymous fields. + } + } + + if typeIsHash(tomlTypeOfGo(frv)) { + fieldsSub = append(fieldsSub, append(start, f.Index...)) + } else { + fieldsDirect = append(fieldsDirect, append(start, f.Index...)) + } + } + } + addFields(rt, rv, nil) + + var writeFields = func(fields [][]int) { + for _, fieldIndex := range fields { + sft := rt.FieldByIndex(fieldIndex) + sf := rv.FieldByIndex(fieldIndex) + if isNil(sf) { + // Don't write anything for nil fields. + continue + } + + opts := getOptions(sft.Tag) + if opts.skip { + continue + } + keyName := sft.Name + if opts.name != "" { + keyName = opts.name + } + if opts.omitempty && isEmpty(sf) { + continue + } + if opts.omitzero && isZero(sf) { + continue + } + + enc.encode(key.add(keyName), sf) + } + } + writeFields(fieldsDirect) + writeFields(fieldsSub) +} + +// tomlTypeName returns the TOML type name of the Go value's type. It is +// used to determine whether the types of array elements are mixed (which is +// forbidden). If the Go value is nil, then it is illegal for it to be an array +// element, and valueIsNil is returned as true. + +// Returns the TOML type of a Go value. The type may be `nil`, which means +// no concrete TOML type could be found. +func tomlTypeOfGo(rv reflect.Value) tomlType { + if isNil(rv) || !rv.IsValid() { + return nil + } + switch rv.Kind() { + case reflect.Bool: + return tomlBool + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64: + return tomlInteger + case reflect.Float32, reflect.Float64: + return tomlFloat + case reflect.Array, reflect.Slice: + if typeEqual(tomlHash, tomlArrayType(rv)) { + return tomlArrayHash + } + return tomlArray + case reflect.Ptr, reflect.Interface: + return tomlTypeOfGo(rv.Elem()) + case reflect.String: + return tomlString + case reflect.Map: + return tomlHash + case reflect.Struct: + switch rv.Interface().(type) { + case time.Time: + return tomlDatetime + case TextMarshaler: + return tomlString + default: + return tomlHash + } + default: + panic("unexpected reflect.Kind: " + rv.Kind().String()) + } +} + +// tomlArrayType returns the element type of a TOML array. The type returned +// may be nil if it cannot be determined (e.g., a nil slice or a zero length +// slize). This function may also panic if it finds a type that cannot be +// expressed in TOML (such as nil elements, heterogeneous arrays or directly +// nested arrays of tables). +func tomlArrayType(rv reflect.Value) tomlType { + if isNil(rv) || !rv.IsValid() || rv.Len() == 0 { + return nil + } + firstType := tomlTypeOfGo(rv.Index(0)) + if firstType == nil { + encPanic(errArrayNilElement) + } + + rvlen := rv.Len() + for i := 1; i < rvlen; i++ { + elem := rv.Index(i) + switch elemType := tomlTypeOfGo(elem); { + case elemType == nil: + encPanic(errArrayNilElement) + case !typeEqual(firstType, elemType): + encPanic(errArrayMixedElementTypes) + } + } + // If we have a nested array, then we must make sure that the nested + // array contains ONLY primitives. + // This checks arbitrarily nested arrays. + if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) { + nest := tomlArrayType(eindirect(rv.Index(0))) + if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) { + encPanic(errArrayNoTable) + } + } + return firstType +} + +type tagOptions struct { + skip bool // "-" + name string + omitempty bool + omitzero bool +} + +func getOptions(tag reflect.StructTag) tagOptions { + t := tag.Get("toml") + if t == "-" { + return tagOptions{skip: true} + } + var opts tagOptions + parts := strings.Split(t, ",") + opts.name = parts[0] + for _, s := range parts[1:] { + switch s { + case "omitempty": + opts.omitempty = true + case "omitzero": + opts.omitzero = true + } + } + return opts +} + +func isZero(rv reflect.Value) bool { + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return rv.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return rv.Uint() == 0 + case reflect.Float32, reflect.Float64: + return rv.Float() == 0.0 + } + return false +} + +func isEmpty(rv reflect.Value) bool { + switch rv.Kind() { + case reflect.Array, reflect.Slice, reflect.Map, reflect.String: + return rv.Len() == 0 + case reflect.Bool: + return !rv.Bool() + } + return false +} + +func (enc *Encoder) newline() { + if enc.hasWritten { + enc.wf("\n") + } +} + +func (enc *Encoder) keyEqElement(key Key, val reflect.Value) { + if len(key) == 0 { + encPanic(errNoKey) + } + panicIfInvalidKey(key) + enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1)) + enc.eElement(val) + enc.newline() +} + +func (enc *Encoder) wf(format string, v ...interface{}) { + if _, err := fmt.Fprintf(enc.w, format, v...); err != nil { + encPanic(err) + } + enc.hasWritten = true +} + +func (enc *Encoder) indentStr(key Key) string { + return strings.Repeat(enc.Indent, len(key)-1) +} + +func encPanic(err error) { + panic(tomlEncodeError{err}) +} + +func eindirect(v reflect.Value) reflect.Value { + switch v.Kind() { + case reflect.Ptr, reflect.Interface: + return eindirect(v.Elem()) + default: + return v + } +} + +func isNil(rv reflect.Value) bool { + switch rv.Kind() { + case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return rv.IsNil() + default: + return false + } +} + +func panicIfInvalidKey(key Key) { + for _, k := range key { + if len(k) == 0 { + encPanic(e("Key '%s' is not a valid table name. Key names "+ + "cannot be empty.", key.maybeQuotedAll())) + } + } +} + +func isValidKeyName(s string) bool { + return len(s) != 0 +} diff --git a/vendor/github.com/BurntSushi/toml/encoding_types.go b/vendor/github.com/BurntSushi/toml/encoding_types.go new file mode 100644 index 0000000..d36e1dd --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/encoding_types.go @@ -0,0 +1,19 @@ +// +build go1.2 + +package toml + +// In order to support Go 1.1, we define our own TextMarshaler and +// TextUnmarshaler types. For Go 1.2+, we just alias them with the +// standard library interfaces. + +import ( + "encoding" +) + +// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here +// so that Go 1.1 can be supported. +type TextMarshaler encoding.TextMarshaler + +// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined +// here so that Go 1.1 can be supported. +type TextUnmarshaler encoding.TextUnmarshaler diff --git a/vendor/github.com/BurntSushi/toml/encoding_types_1.1.go b/vendor/github.com/BurntSushi/toml/encoding_types_1.1.go new file mode 100644 index 0000000..e8d503d --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/encoding_types_1.1.go @@ -0,0 +1,18 @@ +// +build !go1.2 + +package toml + +// These interfaces were introduced in Go 1.2, so we add them manually when +// compiling for Go 1.1. + +// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here +// so that Go 1.1 can be supported. +type TextMarshaler interface { + MarshalText() (text []byte, err error) +} + +// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined +// here so that Go 1.1 can be supported. +type TextUnmarshaler interface { + UnmarshalText(text []byte) error +} diff --git a/vendor/github.com/BurntSushi/toml/lex.go b/vendor/github.com/BurntSushi/toml/lex.go new file mode 100644 index 0000000..e0a742a --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/lex.go @@ -0,0 +1,953 @@ +package toml + +import ( + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +type itemType int + +const ( + itemError itemType = iota + itemNIL // used in the parser to indicate no type + itemEOF + itemText + itemString + itemRawString + itemMultilineString + itemRawMultilineString + itemBool + itemInteger + itemFloat + itemDatetime + itemArray // the start of an array + itemArrayEnd + itemTableStart + itemTableEnd + itemArrayTableStart + itemArrayTableEnd + itemKeyStart + itemCommentStart + itemInlineTableStart + itemInlineTableEnd +) + +const ( + eof = 0 + comma = ',' + tableStart = '[' + tableEnd = ']' + arrayTableStart = '[' + arrayTableEnd = ']' + tableSep = '.' + keySep = '=' + arrayStart = '[' + arrayEnd = ']' + commentStart = '#' + stringStart = '"' + stringEnd = '"' + rawStringStart = '\'' + rawStringEnd = '\'' + inlineTableStart = '{' + inlineTableEnd = '}' +) + +type stateFn func(lx *lexer) stateFn + +type lexer struct { + input string + start int + pos int + line int + state stateFn + items chan item + + // Allow for backing up up to three runes. + // This is necessary because TOML contains 3-rune tokens (""" and '''). + prevWidths [3]int + nprev int // how many of prevWidths are in use + // If we emit an eof, we can still back up, but it is not OK to call + // next again. + atEOF bool + + // A stack of state functions used to maintain context. + // The idea is to reuse parts of the state machine in various places. + // For example, values can appear at the top level or within arbitrarily + // nested arrays. The last state on the stack is used after a value has + // been lexed. Similarly for comments. + stack []stateFn +} + +type item struct { + typ itemType + val string + line int +} + +func (lx *lexer) nextItem() item { + for { + select { + case item := <-lx.items: + return item + default: + lx.state = lx.state(lx) + } + } +} + +func lex(input string) *lexer { + lx := &lexer{ + input: input, + state: lexTop, + line: 1, + items: make(chan item, 10), + stack: make([]stateFn, 0, 10), + } + return lx +} + +func (lx *lexer) push(state stateFn) { + lx.stack = append(lx.stack, state) +} + +func (lx *lexer) pop() stateFn { + if len(lx.stack) == 0 { + return lx.errorf("BUG in lexer: no states to pop") + } + last := lx.stack[len(lx.stack)-1] + lx.stack = lx.stack[0 : len(lx.stack)-1] + return last +} + +func (lx *lexer) current() string { + return lx.input[lx.start:lx.pos] +} + +func (lx *lexer) emit(typ itemType) { + lx.items <- item{typ, lx.current(), lx.line} + lx.start = lx.pos +} + +func (lx *lexer) emitTrim(typ itemType) { + lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line} + lx.start = lx.pos +} + +func (lx *lexer) next() (r rune) { + if lx.atEOF { + panic("next called after EOF") + } + if lx.pos >= len(lx.input) { + lx.atEOF = true + return eof + } + + if lx.input[lx.pos] == '\n' { + lx.line++ + } + lx.prevWidths[2] = lx.prevWidths[1] + lx.prevWidths[1] = lx.prevWidths[0] + if lx.nprev < 3 { + lx.nprev++ + } + r, w := utf8.DecodeRuneInString(lx.input[lx.pos:]) + lx.prevWidths[0] = w + lx.pos += w + return r +} + +// ignore skips over the pending input before this point. +func (lx *lexer) ignore() { + lx.start = lx.pos +} + +// backup steps back one rune. Can be called only twice between calls to next. +func (lx *lexer) backup() { + if lx.atEOF { + lx.atEOF = false + return + } + if lx.nprev < 1 { + panic("backed up too far") + } + w := lx.prevWidths[0] + lx.prevWidths[0] = lx.prevWidths[1] + lx.prevWidths[1] = lx.prevWidths[2] + lx.nprev-- + lx.pos -= w + if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' { + lx.line-- + } +} + +// accept consumes the next rune if it's equal to `valid`. +func (lx *lexer) accept(valid rune) bool { + if lx.next() == valid { + return true + } + lx.backup() + return false +} + +// peek returns but does not consume the next rune in the input. +func (lx *lexer) peek() rune { + r := lx.next() + lx.backup() + return r +} + +// skip ignores all input that matches the given predicate. +func (lx *lexer) skip(pred func(rune) bool) { + for { + r := lx.next() + if pred(r) { + continue + } + lx.backup() + lx.ignore() + return + } +} + +// errorf stops all lexing by emitting an error and returning `nil`. +// Note that any value that is a character is escaped if it's a special +// character (newlines, tabs, etc.). +func (lx *lexer) errorf(format string, values ...interface{}) stateFn { + lx.items <- item{ + itemError, + fmt.Sprintf(format, values...), + lx.line, + } + return nil +} + +// lexTop consumes elements at the top level of TOML data. +func lexTop(lx *lexer) stateFn { + r := lx.next() + if isWhitespace(r) || isNL(r) { + return lexSkip(lx, lexTop) + } + switch r { + case commentStart: + lx.push(lexTop) + return lexCommentStart + case tableStart: + return lexTableStart + case eof: + if lx.pos > lx.start { + return lx.errorf("unexpected EOF") + } + lx.emit(itemEOF) + return nil + } + + // At this point, the only valid item can be a key, so we back up + // and let the key lexer do the rest. + lx.backup() + lx.push(lexTopEnd) + return lexKeyStart +} + +// lexTopEnd is entered whenever a top-level item has been consumed. (A value +// or a table.) It must see only whitespace, and will turn back to lexTop +// upon a newline. If it sees EOF, it will quit the lexer successfully. +func lexTopEnd(lx *lexer) stateFn { + r := lx.next() + switch { + case r == commentStart: + // a comment will read to a newline for us. + lx.push(lexTop) + return lexCommentStart + case isWhitespace(r): + return lexTopEnd + case isNL(r): + lx.ignore() + return lexTop + case r == eof: + lx.emit(itemEOF) + return nil + } + return lx.errorf("expected a top-level item to end with a newline, "+ + "comment, or EOF, but got %q instead", r) +} + +// lexTable lexes the beginning of a table. Namely, it makes sure that +// it starts with a character other than '.' and ']'. +// It assumes that '[' has already been consumed. +// It also handles the case that this is an item in an array of tables. +// e.g., '[[name]]'. +func lexTableStart(lx *lexer) stateFn { + if lx.peek() == arrayTableStart { + lx.next() + lx.emit(itemArrayTableStart) + lx.push(lexArrayTableEnd) + } else { + lx.emit(itemTableStart) + lx.push(lexTableEnd) + } + return lexTableNameStart +} + +func lexTableEnd(lx *lexer) stateFn { + lx.emit(itemTableEnd) + return lexTopEnd +} + +func lexArrayTableEnd(lx *lexer) stateFn { + if r := lx.next(); r != arrayTableEnd { + return lx.errorf("expected end of table array name delimiter %q, "+ + "but got %q instead", arrayTableEnd, r) + } + lx.emit(itemArrayTableEnd) + return lexTopEnd +} + +func lexTableNameStart(lx *lexer) stateFn { + lx.skip(isWhitespace) + switch r := lx.peek(); { + case r == tableEnd || r == eof: + return lx.errorf("unexpected end of table name " + + "(table names cannot be empty)") + case r == tableSep: + return lx.errorf("unexpected table separator " + + "(table names cannot be empty)") + case r == stringStart || r == rawStringStart: + lx.ignore() + lx.push(lexTableNameEnd) + return lexValue // reuse string lexing + default: + return lexBareTableName + } +} + +// lexBareTableName lexes the name of a table. It assumes that at least one +// valid character for the table has already been read. +func lexBareTableName(lx *lexer) stateFn { + r := lx.next() + if isBareKeyChar(r) { + return lexBareTableName + } + lx.backup() + lx.emit(itemText) + return lexTableNameEnd +} + +// lexTableNameEnd reads the end of a piece of a table name, optionally +// consuming whitespace. +func lexTableNameEnd(lx *lexer) stateFn { + lx.skip(isWhitespace) + switch r := lx.next(); { + case isWhitespace(r): + return lexTableNameEnd + case r == tableSep: + lx.ignore() + return lexTableNameStart + case r == tableEnd: + return lx.pop() + default: + return lx.errorf("expected '.' or ']' to end table name, "+ + "but got %q instead", r) + } +} + +// lexKeyStart consumes a key name up until the first non-whitespace character. +// lexKeyStart will ignore whitespace. +func lexKeyStart(lx *lexer) stateFn { + r := lx.peek() + switch { + case r == keySep: + return lx.errorf("unexpected key separator %q", keySep) + case isWhitespace(r) || isNL(r): + lx.next() + return lexSkip(lx, lexKeyStart) + case r == stringStart || r == rawStringStart: + lx.ignore() + lx.emit(itemKeyStart) + lx.push(lexKeyEnd) + return lexValue // reuse string lexing + default: + lx.ignore() + lx.emit(itemKeyStart) + return lexBareKey + } +} + +// lexBareKey consumes the text of a bare key. Assumes that the first character +// (which is not whitespace) has not yet been consumed. +func lexBareKey(lx *lexer) stateFn { + switch r := lx.next(); { + case isBareKeyChar(r): + return lexBareKey + case isWhitespace(r): + lx.backup() + lx.emit(itemText) + return lexKeyEnd + case r == keySep: + lx.backup() + lx.emit(itemText) + return lexKeyEnd + default: + return lx.errorf("bare keys cannot contain %q", r) + } +} + +// lexKeyEnd consumes the end of a key and trims whitespace (up to the key +// separator). +func lexKeyEnd(lx *lexer) stateFn { + switch r := lx.next(); { + case r == keySep: + return lexSkip(lx, lexValue) + case isWhitespace(r): + return lexSkip(lx, lexKeyEnd) + default: + return lx.errorf("expected key separator %q, but got %q instead", + keySep, r) + } +} + +// lexValue starts the consumption of a value anywhere a value is expected. +// lexValue will ignore whitespace. +// After a value is lexed, the last state on the next is popped and returned. +func lexValue(lx *lexer) stateFn { + // We allow whitespace to precede a value, but NOT newlines. + // In array syntax, the array states are responsible for ignoring newlines. + r := lx.next() + switch { + case isWhitespace(r): + return lexSkip(lx, lexValue) + case isDigit(r): + lx.backup() // avoid an extra state and use the same as above + return lexNumberOrDateStart + } + switch r { + case arrayStart: + lx.ignore() + lx.emit(itemArray) + return lexArrayValue + case inlineTableStart: + lx.ignore() + lx.emit(itemInlineTableStart) + return lexInlineTableValue + case stringStart: + if lx.accept(stringStart) { + if lx.accept(stringStart) { + lx.ignore() // Ignore """ + return lexMultilineString + } + lx.backup() + } + lx.ignore() // ignore the '"' + return lexString + case rawStringStart: + if lx.accept(rawStringStart) { + if lx.accept(rawStringStart) { + lx.ignore() // Ignore """ + return lexMultilineRawString + } + lx.backup() + } + lx.ignore() // ignore the "'" + return lexRawString + case '+', '-': + return lexNumberStart + case '.': // special error case, be kind to users + return lx.errorf("floats must start with a digit, not '.'") + } + if unicode.IsLetter(r) { + // Be permissive here; lexBool will give a nice error if the + // user wrote something like + // x = foo + // (i.e. not 'true' or 'false' but is something else word-like.) + lx.backup() + return lexBool + } + return lx.errorf("expected value but found %q instead", r) +} + +// lexArrayValue consumes one value in an array. It assumes that '[' or ',' +// have already been consumed. All whitespace and newlines are ignored. +func lexArrayValue(lx *lexer) stateFn { + r := lx.next() + switch { + case isWhitespace(r) || isNL(r): + return lexSkip(lx, lexArrayValue) + case r == commentStart: + lx.push(lexArrayValue) + return lexCommentStart + case r == comma: + return lx.errorf("unexpected comma") + case r == arrayEnd: + // NOTE(caleb): The spec isn't clear about whether you can have + // a trailing comma or not, so we'll allow it. + return lexArrayEnd + } + + lx.backup() + lx.push(lexArrayValueEnd) + return lexValue +} + +// lexArrayValueEnd consumes everything between the end of an array value and +// the next value (or the end of the array): it ignores whitespace and newlines +// and expects either a ',' or a ']'. +func lexArrayValueEnd(lx *lexer) stateFn { + r := lx.next() + switch { + case isWhitespace(r) || isNL(r): + return lexSkip(lx, lexArrayValueEnd) + case r == commentStart: + lx.push(lexArrayValueEnd) + return lexCommentStart + case r == comma: + lx.ignore() + return lexArrayValue // move on to the next value + case r == arrayEnd: + return lexArrayEnd + } + return lx.errorf( + "expected a comma or array terminator %q, but got %q instead", + arrayEnd, r, + ) +} + +// lexArrayEnd finishes the lexing of an array. +// It assumes that a ']' has just been consumed. +func lexArrayEnd(lx *lexer) stateFn { + lx.ignore() + lx.emit(itemArrayEnd) + return lx.pop() +} + +// lexInlineTableValue consumes one key/value pair in an inline table. +// It assumes that '{' or ',' have already been consumed. Whitespace is ignored. +func lexInlineTableValue(lx *lexer) stateFn { + r := lx.next() + switch { + case isWhitespace(r): + return lexSkip(lx, lexInlineTableValue) + case isNL(r): + return lx.errorf("newlines not allowed within inline tables") + case r == commentStart: + lx.push(lexInlineTableValue) + return lexCommentStart + case r == comma: + return lx.errorf("unexpected comma") + case r == inlineTableEnd: + return lexInlineTableEnd + } + lx.backup() + lx.push(lexInlineTableValueEnd) + return lexKeyStart +} + +// lexInlineTableValueEnd consumes everything between the end of an inline table +// key/value pair and the next pair (or the end of the table): +// it ignores whitespace and expects either a ',' or a '}'. +func lexInlineTableValueEnd(lx *lexer) stateFn { + r := lx.next() + switch { + case isWhitespace(r): + return lexSkip(lx, lexInlineTableValueEnd) + case isNL(r): + return lx.errorf("newlines not allowed within inline tables") + case r == commentStart: + lx.push(lexInlineTableValueEnd) + return lexCommentStart + case r == comma: + lx.ignore() + return lexInlineTableValue + case r == inlineTableEnd: + return lexInlineTableEnd + } + return lx.errorf("expected a comma or an inline table terminator %q, "+ + "but got %q instead", inlineTableEnd, r) +} + +// lexInlineTableEnd finishes the lexing of an inline table. +// It assumes that a '}' has just been consumed. +func lexInlineTableEnd(lx *lexer) stateFn { + lx.ignore() + lx.emit(itemInlineTableEnd) + return lx.pop() +} + +// lexString consumes the inner contents of a string. It assumes that the +// beginning '"' has already been consumed and ignored. +func lexString(lx *lexer) stateFn { + r := lx.next() + switch { + case r == eof: + return lx.errorf("unexpected EOF") + case isNL(r): + return lx.errorf("strings cannot contain newlines") + case r == '\\': + lx.push(lexString) + return lexStringEscape + case r == stringEnd: + lx.backup() + lx.emit(itemString) + lx.next() + lx.ignore() + return lx.pop() + } + return lexString +} + +// lexMultilineString consumes the inner contents of a string. It assumes that +// the beginning '"""' has already been consumed and ignored. +func lexMultilineString(lx *lexer) stateFn { + switch lx.next() { + case eof: + return lx.errorf("unexpected EOF") + case '\\': + return lexMultilineStringEscape + case stringEnd: + if lx.accept(stringEnd) { + if lx.accept(stringEnd) { + lx.backup() + lx.backup() + lx.backup() + lx.emit(itemMultilineString) + lx.next() + lx.next() + lx.next() + lx.ignore() + return lx.pop() + } + lx.backup() + } + } + return lexMultilineString +} + +// lexRawString consumes a raw string. Nothing can be escaped in such a string. +// It assumes that the beginning "'" has already been consumed and ignored. +func lexRawString(lx *lexer) stateFn { + r := lx.next() + switch { + case r == eof: + return lx.errorf("unexpected EOF") + case isNL(r): + return lx.errorf("strings cannot contain newlines") + case r == rawStringEnd: + lx.backup() + lx.emit(itemRawString) + lx.next() + lx.ignore() + return lx.pop() + } + return lexRawString +} + +// lexMultilineRawString consumes a raw string. Nothing can be escaped in such +// a string. It assumes that the beginning "'''" has already been consumed and +// ignored. +func lexMultilineRawString(lx *lexer) stateFn { + switch lx.next() { + case eof: + return lx.errorf("unexpected EOF") + case rawStringEnd: + if lx.accept(rawStringEnd) { + if lx.accept(rawStringEnd) { + lx.backup() + lx.backup() + lx.backup() + lx.emit(itemRawMultilineString) + lx.next() + lx.next() + lx.next() + lx.ignore() + return lx.pop() + } + lx.backup() + } + } + return lexMultilineRawString +} + +// lexMultilineStringEscape consumes an escaped character. It assumes that the +// preceding '\\' has already been consumed. +func lexMultilineStringEscape(lx *lexer) stateFn { + // Handle the special case first: + if isNL(lx.next()) { + return lexMultilineString + } + lx.backup() + lx.push(lexMultilineString) + return lexStringEscape(lx) +} + +func lexStringEscape(lx *lexer) stateFn { + r := lx.next() + switch r { + case 'b': + fallthrough + case 't': + fallthrough + case 'n': + fallthrough + case 'f': + fallthrough + case 'r': + fallthrough + case '"': + fallthrough + case '\\': + return lx.pop() + case 'u': + return lexShortUnicodeEscape + case 'U': + return lexLongUnicodeEscape + } + return lx.errorf("invalid escape character %q; only the following "+ + "escape characters are allowed: "+ + `\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX`, r) +} + +func lexShortUnicodeEscape(lx *lexer) stateFn { + var r rune + for i := 0; i < 4; i++ { + r = lx.next() + if !isHexadecimal(r) { + return lx.errorf(`expected four hexadecimal digits after '\u', `+ + "but got %q instead", lx.current()) + } + } + return lx.pop() +} + +func lexLongUnicodeEscape(lx *lexer) stateFn { + var r rune + for i := 0; i < 8; i++ { + r = lx.next() + if !isHexadecimal(r) { + return lx.errorf(`expected eight hexadecimal digits after '\U', `+ + "but got %q instead", lx.current()) + } + } + return lx.pop() +} + +// lexNumberOrDateStart consumes either an integer, a float, or datetime. +func lexNumberOrDateStart(lx *lexer) stateFn { + r := lx.next() + if isDigit(r) { + return lexNumberOrDate + } + switch r { + case '_': + return lexNumber + case 'e', 'E': + return lexFloat + case '.': + return lx.errorf("floats must start with a digit, not '.'") + } + return lx.errorf("expected a digit but got %q", r) +} + +// lexNumberOrDate consumes either an integer, float or datetime. +func lexNumberOrDate(lx *lexer) stateFn { + r := lx.next() + if isDigit(r) { + return lexNumberOrDate + } + switch r { + case '-': + return lexDatetime + case '_': + return lexNumber + case '.', 'e', 'E': + return lexFloat + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexDatetime consumes a Datetime, to a first approximation. +// The parser validates that it matches one of the accepted formats. +func lexDatetime(lx *lexer) stateFn { + r := lx.next() + if isDigit(r) { + return lexDatetime + } + switch r { + case '-', 'T', ':', '.', 'Z', '+': + return lexDatetime + } + + lx.backup() + lx.emit(itemDatetime) + return lx.pop() +} + +// lexNumberStart consumes either an integer or a float. It assumes that a sign +// has already been read, but that *no* digits have been consumed. +// lexNumberStart will move to the appropriate integer or float states. +func lexNumberStart(lx *lexer) stateFn { + // We MUST see a digit. Even floats have to start with a digit. + r := lx.next() + if !isDigit(r) { + if r == '.' { + return lx.errorf("floats must start with a digit, not '.'") + } + return lx.errorf("expected a digit but got %q", r) + } + return lexNumber +} + +// lexNumber consumes an integer or a float after seeing the first digit. +func lexNumber(lx *lexer) stateFn { + r := lx.next() + if isDigit(r) { + return lexNumber + } + switch r { + case '_': + return lexNumber + case '.', 'e', 'E': + return lexFloat + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexFloat consumes the elements of a float. It allows any sequence of +// float-like characters, so floats emitted by the lexer are only a first +// approximation and must be validated by the parser. +func lexFloat(lx *lexer) stateFn { + r := lx.next() + if isDigit(r) { + return lexFloat + } + switch r { + case '_', '.', '-', '+', 'e', 'E': + return lexFloat + } + + lx.backup() + lx.emit(itemFloat) + return lx.pop() +} + +// lexBool consumes a bool string: 'true' or 'false. +func lexBool(lx *lexer) stateFn { + var rs []rune + for { + r := lx.next() + if !unicode.IsLetter(r) { + lx.backup() + break + } + rs = append(rs, r) + } + s := string(rs) + switch s { + case "true", "false": + lx.emit(itemBool) + return lx.pop() + } + return lx.errorf("expected value but found %q instead", s) +} + +// lexCommentStart begins the lexing of a comment. It will emit +// itemCommentStart and consume no characters, passing control to lexComment. +func lexCommentStart(lx *lexer) stateFn { + lx.ignore() + lx.emit(itemCommentStart) + return lexComment +} + +// lexComment lexes an entire comment. It assumes that '#' has been consumed. +// It will consume *up to* the first newline character, and pass control +// back to the last state on the stack. +func lexComment(lx *lexer) stateFn { + r := lx.peek() + if isNL(r) || r == eof { + lx.emit(itemText) + return lx.pop() + } + lx.next() + return lexComment +} + +// lexSkip ignores all slurped input and moves on to the next state. +func lexSkip(lx *lexer, nextState stateFn) stateFn { + return func(lx *lexer) stateFn { + lx.ignore() + return nextState + } +} + +// isWhitespace returns true if `r` is a whitespace character according +// to the spec. +func isWhitespace(r rune) bool { + return r == '\t' || r == ' ' +} + +func isNL(r rune) bool { + return r == '\n' || r == '\r' +} + +func isDigit(r rune) bool { + return r >= '0' && r <= '9' +} + +func isHexadecimal(r rune) bool { + return (r >= '0' && r <= '9') || + (r >= 'a' && r <= 'f') || + (r >= 'A' && r <= 'F') +} + +func isBareKeyChar(r rune) bool { + return (r >= 'A' && r <= 'Z') || + (r >= 'a' && r <= 'z') || + (r >= '0' && r <= '9') || + r == '_' || + r == '-' +} + +func (itype itemType) String() string { + switch itype { + case itemError: + return "Error" + case itemNIL: + return "NIL" + case itemEOF: + return "EOF" + case itemText: + return "Text" + case itemString, itemRawString, itemMultilineString, itemRawMultilineString: + return "String" + case itemBool: + return "Bool" + case itemInteger: + return "Integer" + case itemFloat: + return "Float" + case itemDatetime: + return "DateTime" + case itemTableStart: + return "TableStart" + case itemTableEnd: + return "TableEnd" + case itemKeyStart: + return "KeyStart" + case itemArray: + return "Array" + case itemArrayEnd: + return "ArrayEnd" + case itemCommentStart: + return "CommentStart" + } + panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype))) +} + +func (item item) String() string { + return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val) +} diff --git a/vendor/github.com/BurntSushi/toml/parse.go b/vendor/github.com/BurntSushi/toml/parse.go new file mode 100644 index 0000000..50869ef --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/parse.go @@ -0,0 +1,592 @@ +package toml + +import ( + "fmt" + "strconv" + "strings" + "time" + "unicode" + "unicode/utf8" +) + +type parser struct { + mapping map[string]interface{} + types map[string]tomlType + lx *lexer + + // A list of keys in the order that they appear in the TOML data. + ordered []Key + + // the full key for the current hash in scope + context Key + + // the base key name for everything except hashes + currentKey string + + // rough approximation of line number + approxLine int + + // A map of 'key.group.names' to whether they were created implicitly. + implicits map[string]bool +} + +type parseError string + +func (pe parseError) Error() string { + return string(pe) +} + +func parse(data string) (p *parser, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + if err, ok = r.(parseError); ok { + return + } + panic(r) + } + }() + + p = &parser{ + mapping: make(map[string]interface{}), + types: make(map[string]tomlType), + lx: lex(data), + ordered: make([]Key, 0), + implicits: make(map[string]bool), + } + for { + item := p.next() + if item.typ == itemEOF { + break + } + p.topLevel(item) + } + + return p, nil +} + +func (p *parser) panicf(format string, v ...interface{}) { + msg := fmt.Sprintf("Near line %d (last key parsed '%s'): %s", + p.approxLine, p.current(), fmt.Sprintf(format, v...)) + panic(parseError(msg)) +} + +func (p *parser) next() item { + it := p.lx.nextItem() + if it.typ == itemError { + p.panicf("%s", it.val) + } + return it +} + +func (p *parser) bug(format string, v ...interface{}) { + panic(fmt.Sprintf("BUG: "+format+"\n\n", v...)) +} + +func (p *parser) expect(typ itemType) item { + it := p.next() + p.assertEqual(typ, it.typ) + return it +} + +func (p *parser) assertEqual(expected, got itemType) { + if expected != got { + p.bug("Expected '%s' but got '%s'.", expected, got) + } +} + +func (p *parser) topLevel(item item) { + switch item.typ { + case itemCommentStart: + p.approxLine = item.line + p.expect(itemText) + case itemTableStart: + kg := p.next() + p.approxLine = kg.line + + var key Key + for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() { + key = append(key, p.keyString(kg)) + } + p.assertEqual(itemTableEnd, kg.typ) + + p.establishContext(key, false) + p.setType("", tomlHash) + p.ordered = append(p.ordered, key) + case itemArrayTableStart: + kg := p.next() + p.approxLine = kg.line + + var key Key + for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() { + key = append(key, p.keyString(kg)) + } + p.assertEqual(itemArrayTableEnd, kg.typ) + + p.establishContext(key, true) + p.setType("", tomlArrayHash) + p.ordered = append(p.ordered, key) + case itemKeyStart: + kname := p.next() + p.approxLine = kname.line + p.currentKey = p.keyString(kname) + + val, typ := p.value(p.next()) + p.setValue(p.currentKey, val) + p.setType(p.currentKey, typ) + p.ordered = append(p.ordered, p.context.add(p.currentKey)) + p.currentKey = "" + default: + p.bug("Unexpected type at top level: %s", item.typ) + } +} + +// Gets a string for a key (or part of a key in a table name). +func (p *parser) keyString(it item) string { + switch it.typ { + case itemText: + return it.val + case itemString, itemMultilineString, + itemRawString, itemRawMultilineString: + s, _ := p.value(it) + return s.(string) + default: + p.bug("Unexpected key type: %s", it.typ) + panic("unreachable") + } +} + +// value translates an expected value from the lexer into a Go value wrapped +// as an empty interface. +func (p *parser) value(it item) (interface{}, tomlType) { + switch it.typ { + case itemString: + return p.replaceEscapes(it.val), p.typeOfPrimitive(it) + case itemMultilineString: + trimmed := stripFirstNewline(stripEscapedWhitespace(it.val)) + return p.replaceEscapes(trimmed), p.typeOfPrimitive(it) + case itemRawString: + return it.val, p.typeOfPrimitive(it) + case itemRawMultilineString: + return stripFirstNewline(it.val), p.typeOfPrimitive(it) + case itemBool: + switch it.val { + case "true": + return true, p.typeOfPrimitive(it) + case "false": + return false, p.typeOfPrimitive(it) + } + p.bug("Expected boolean value, but got '%s'.", it.val) + case itemInteger: + if !numUnderscoresOK(it.val) { + p.panicf("Invalid integer %q: underscores must be surrounded by digits", + it.val) + } + val := strings.Replace(it.val, "_", "", -1) + num, err := strconv.ParseInt(val, 10, 64) + if err != nil { + // Distinguish integer values. Normally, it'd be a bug if the lexer + // provides an invalid integer, but it's possible that the number is + // out of range of valid values (which the lexer cannot determine). + // So mark the former as a bug but the latter as a legitimate user + // error. + if e, ok := err.(*strconv.NumError); ok && + e.Err == strconv.ErrRange { + + p.panicf("Integer '%s' is out of the range of 64-bit "+ + "signed integers.", it.val) + } else { + p.bug("Expected integer value, but got '%s'.", it.val) + } + } + return num, p.typeOfPrimitive(it) + case itemFloat: + parts := strings.FieldsFunc(it.val, func(r rune) bool { + switch r { + case '.', 'e', 'E': + return true + } + return false + }) + for _, part := range parts { + if !numUnderscoresOK(part) { + p.panicf("Invalid float %q: underscores must be "+ + "surrounded by digits", it.val) + } + } + if !numPeriodsOK(it.val) { + // As a special case, numbers like '123.' or '1.e2', + // which are valid as far as Go/strconv are concerned, + // must be rejected because TOML says that a fractional + // part consists of '.' followed by 1+ digits. + p.panicf("Invalid float %q: '.' must be followed "+ + "by one or more digits", it.val) + } + val := strings.Replace(it.val, "_", "", -1) + num, err := strconv.ParseFloat(val, 64) + if err != nil { + if e, ok := err.(*strconv.NumError); ok && + e.Err == strconv.ErrRange { + + p.panicf("Float '%s' is out of the range of 64-bit "+ + "IEEE-754 floating-point numbers.", it.val) + } else { + p.panicf("Invalid float value: %q", it.val) + } + } + return num, p.typeOfPrimitive(it) + case itemDatetime: + var t time.Time + var ok bool + var err error + for _, format := range []string{ + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05", + "2006-01-02", + } { + t, err = time.ParseInLocation(format, it.val, time.Local) + if err == nil { + ok = true + break + } + } + if !ok { + p.panicf("Invalid TOML Datetime: %q.", it.val) + } + return t, p.typeOfPrimitive(it) + case itemArray: + array := make([]interface{}, 0) + types := make([]tomlType, 0) + + for it = p.next(); it.typ != itemArrayEnd; it = p.next() { + if it.typ == itemCommentStart { + p.expect(itemText) + continue + } + + val, typ := p.value(it) + array = append(array, val) + types = append(types, typ) + } + return array, p.typeOfArray(types) + case itemInlineTableStart: + var ( + hash = make(map[string]interface{}) + outerContext = p.context + outerKey = p.currentKey + ) + + p.context = append(p.context, p.currentKey) + p.currentKey = "" + for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() { + if it.typ != itemKeyStart { + p.bug("Expected key start but instead found %q, around line %d", + it.val, p.approxLine) + } + if it.typ == itemCommentStart { + p.expect(itemText) + continue + } + + // retrieve key + k := p.next() + p.approxLine = k.line + kname := p.keyString(k) + + // retrieve value + p.currentKey = kname + val, typ := p.value(p.next()) + // make sure we keep metadata up to date + p.setType(kname, typ) + p.ordered = append(p.ordered, p.context.add(p.currentKey)) + hash[kname] = val + } + p.context = outerContext + p.currentKey = outerKey + return hash, tomlHash + } + p.bug("Unexpected value type: %s", it.typ) + panic("unreachable") +} + +// numUnderscoresOK checks whether each underscore in s is surrounded by +// characters that are not underscores. +func numUnderscoresOK(s string) bool { + accept := false + for _, r := range s { + if r == '_' { + if !accept { + return false + } + accept = false + continue + } + accept = true + } + return accept +} + +// numPeriodsOK checks whether every period in s is followed by a digit. +func numPeriodsOK(s string) bool { + period := false + for _, r := range s { + if period && !isDigit(r) { + return false + } + period = r == '.' + } + return !period +} + +// establishContext sets the current context of the parser, +// where the context is either a hash or an array of hashes. Which one is +// set depends on the value of the `array` parameter. +// +// Establishing the context also makes sure that the key isn't a duplicate, and +// will create implicit hashes automatically. +func (p *parser) establishContext(key Key, array bool) { + var ok bool + + // Always start at the top level and drill down for our context. + hashContext := p.mapping + keyContext := make(Key, 0) + + // We only need implicit hashes for key[0:-1] + for _, k := range key[0 : len(key)-1] { + _, ok = hashContext[k] + keyContext = append(keyContext, k) + + // No key? Make an implicit hash and move on. + if !ok { + p.addImplicit(keyContext) + hashContext[k] = make(map[string]interface{}) + } + + // If the hash context is actually an array of tables, then set + // the hash context to the last element in that array. + // + // Otherwise, it better be a table, since this MUST be a key group (by + // virtue of it not being the last element in a key). + switch t := hashContext[k].(type) { + case []map[string]interface{}: + hashContext = t[len(t)-1] + case map[string]interface{}: + hashContext = t + default: + p.panicf("Key '%s' was already created as a hash.", keyContext) + } + } + + p.context = keyContext + if array { + // If this is the first element for this array, then allocate a new + // list of tables for it. + k := key[len(key)-1] + if _, ok := hashContext[k]; !ok { + hashContext[k] = make([]map[string]interface{}, 0, 5) + } + + // Add a new table. But make sure the key hasn't already been used + // for something else. + if hash, ok := hashContext[k].([]map[string]interface{}); ok { + hashContext[k] = append(hash, make(map[string]interface{})) + } else { + p.panicf("Key '%s' was already created and cannot be used as "+ + "an array.", keyContext) + } + } else { + p.setValue(key[len(key)-1], make(map[string]interface{})) + } + p.context = append(p.context, key[len(key)-1]) +} + +// setValue sets the given key to the given value in the current context. +// It will make sure that the key hasn't already been defined, account for +// implicit key groups. +func (p *parser) setValue(key string, value interface{}) { + var tmpHash interface{} + var ok bool + + hash := p.mapping + keyContext := make(Key, 0) + for _, k := range p.context { + keyContext = append(keyContext, k) + if tmpHash, ok = hash[k]; !ok { + p.bug("Context for key '%s' has not been established.", keyContext) + } + switch t := tmpHash.(type) { + case []map[string]interface{}: + // The context is a table of hashes. Pick the most recent table + // defined as the current hash. + hash = t[len(t)-1] + case map[string]interface{}: + hash = t + default: + p.bug("Expected hash to have type 'map[string]interface{}', but "+ + "it has '%T' instead.", tmpHash) + } + } + keyContext = append(keyContext, key) + + if _, ok := hash[key]; ok { + // Typically, if the given key has already been set, then we have + // to raise an error since duplicate keys are disallowed. However, + // it's possible that a key was previously defined implicitly. In this + // case, it is allowed to be redefined concretely. (See the + // `tests/valid/implicit-and-explicit-after.toml` test in `toml-test`.) + // + // But we have to make sure to stop marking it as an implicit. (So that + // another redefinition provokes an error.) + // + // Note that since it has already been defined (as a hash), we don't + // want to overwrite it. So our business is done. + if p.isImplicit(keyContext) { + p.removeImplicit(keyContext) + return + } + + // Otherwise, we have a concrete key trying to override a previous + // key, which is *always* wrong. + p.panicf("Key '%s' has already been defined.", keyContext) + } + hash[key] = value +} + +// setType sets the type of a particular value at a given key. +// It should be called immediately AFTER setValue. +// +// Note that if `key` is empty, then the type given will be applied to the +// current context (which is either a table or an array of tables). +func (p *parser) setType(key string, typ tomlType) { + keyContext := make(Key, 0, len(p.context)+1) + for _, k := range p.context { + keyContext = append(keyContext, k) + } + if len(key) > 0 { // allow type setting for hashes + keyContext = append(keyContext, key) + } + p.types[keyContext.String()] = typ +} + +// addImplicit sets the given Key as having been created implicitly. +func (p *parser) addImplicit(key Key) { + p.implicits[key.String()] = true +} + +// removeImplicit stops tagging the given key as having been implicitly +// created. +func (p *parser) removeImplicit(key Key) { + p.implicits[key.String()] = false +} + +// isImplicit returns true if the key group pointed to by the key was created +// implicitly. +func (p *parser) isImplicit(key Key) bool { + return p.implicits[key.String()] +} + +// current returns the full key name of the current context. +func (p *parser) current() string { + if len(p.currentKey) == 0 { + return p.context.String() + } + if len(p.context) == 0 { + return p.currentKey + } + return fmt.Sprintf("%s.%s", p.context, p.currentKey) +} + +func stripFirstNewline(s string) string { + if len(s) == 0 || s[0] != '\n' { + return s + } + return s[1:] +} + +func stripEscapedWhitespace(s string) string { + esc := strings.Split(s, "\\\n") + if len(esc) > 1 { + for i := 1; i < len(esc); i++ { + esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace) + } + } + return strings.Join(esc, "") +} + +func (p *parser) replaceEscapes(str string) string { + var replaced []rune + s := []byte(str) + r := 0 + for r < len(s) { + if s[r] != '\\' { + c, size := utf8.DecodeRune(s[r:]) + r += size + replaced = append(replaced, c) + continue + } + r += 1 + if r >= len(s) { + p.bug("Escape sequence at end of string.") + return "" + } + switch s[r] { + default: + p.bug("Expected valid escape code after \\, but got %q.", s[r]) + return "" + case 'b': + replaced = append(replaced, rune(0x0008)) + r += 1 + case 't': + replaced = append(replaced, rune(0x0009)) + r += 1 + case 'n': + replaced = append(replaced, rune(0x000A)) + r += 1 + case 'f': + replaced = append(replaced, rune(0x000C)) + r += 1 + case 'r': + replaced = append(replaced, rune(0x000D)) + r += 1 + case '"': + replaced = append(replaced, rune(0x0022)) + r += 1 + case '\\': + replaced = append(replaced, rune(0x005C)) + r += 1 + case 'u': + // At this point, we know we have a Unicode escape of the form + // `uXXXX` at [r, r+5). (Because the lexer guarantees this + // for us.) + escaped := p.asciiEscapeToUnicode(s[r+1 : r+5]) + replaced = append(replaced, escaped) + r += 5 + case 'U': + // At this point, we know we have a Unicode escape of the form + // `uXXXX` at [r, r+9). (Because the lexer guarantees this + // for us.) + escaped := p.asciiEscapeToUnicode(s[r+1 : r+9]) + replaced = append(replaced, escaped) + r += 9 + } + } + return string(replaced) +} + +func (p *parser) asciiEscapeToUnicode(bs []byte) rune { + s := string(bs) + hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32) + if err != nil { + p.bug("Could not parse '%s' as a hexadecimal number, but the "+ + "lexer claims it's OK: %s", s, err) + } + if !utf8.ValidRune(rune(hex)) { + p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s) + } + return rune(hex) +} + +func isStringType(ty itemType) bool { + return ty == itemString || ty == itemMultilineString || + ty == itemRawString || ty == itemRawMultilineString +} diff --git a/vendor/github.com/BurntSushi/toml/session.vim b/vendor/github.com/BurntSushi/toml/session.vim new file mode 100644 index 0000000..562164b --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/session.vim @@ -0,0 +1 @@ +au BufWritePost *.go silent!make tags > /dev/null 2>&1 diff --git a/vendor/github.com/BurntSushi/toml/type_check.go b/vendor/github.com/BurntSushi/toml/type_check.go new file mode 100644 index 0000000..c73f8af --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/type_check.go @@ -0,0 +1,91 @@ +package toml + +// tomlType represents any Go type that corresponds to a TOML type. +// While the first draft of the TOML spec has a simplistic type system that +// probably doesn't need this level of sophistication, we seem to be militating +// toward adding real composite types. +type tomlType interface { + typeString() string +} + +// typeEqual accepts any two types and returns true if they are equal. +func typeEqual(t1, t2 tomlType) bool { + if t1 == nil || t2 == nil { + return false + } + return t1.typeString() == t2.typeString() +} + +func typeIsHash(t tomlType) bool { + return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash) +} + +type tomlBaseType string + +func (btype tomlBaseType) typeString() string { + return string(btype) +} + +func (btype tomlBaseType) String() string { + return btype.typeString() +} + +var ( + tomlInteger tomlBaseType = "Integer" + tomlFloat tomlBaseType = "Float" + tomlDatetime tomlBaseType = "Datetime" + tomlString tomlBaseType = "String" + tomlBool tomlBaseType = "Bool" + tomlArray tomlBaseType = "Array" + tomlHash tomlBaseType = "Hash" + tomlArrayHash tomlBaseType = "ArrayHash" +) + +// typeOfPrimitive returns a tomlType of any primitive value in TOML. +// Primitive values are: Integer, Float, Datetime, String and Bool. +// +// Passing a lexer item other than the following will cause a BUG message +// to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime. +func (p *parser) typeOfPrimitive(lexItem item) tomlType { + switch lexItem.typ { + case itemInteger: + return tomlInteger + case itemFloat: + return tomlFloat + case itemDatetime: + return tomlDatetime + case itemString: + return tomlString + case itemMultilineString: + return tomlString + case itemRawString: + return tomlString + case itemRawMultilineString: + return tomlString + case itemBool: + return tomlBool + } + p.bug("Cannot infer primitive type of lex item '%s'.", lexItem) + panic("unreachable") +} + +// typeOfArray returns a tomlType for an array given a list of types of its +// values. +// +// In the current spec, if an array is homogeneous, then its type is always +// "Array". If the array is not homogeneous, an error is generated. +func (p *parser) typeOfArray(types []tomlType) tomlType { + // Empty arrays are cool. + if len(types) == 0 { + return tomlArray + } + + theType := types[0] + for _, t := range types[1:] { + if !typeEqual(theType, t) { + p.panicf("Array contains values of type '%s' and '%s', but "+ + "arrays must be homogeneous.", theType, t) + } + } + return tomlArray +} diff --git a/vendor/github.com/BurntSushi/toml/type_fields.go b/vendor/github.com/BurntSushi/toml/type_fields.go new file mode 100644 index 0000000..608997c --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/type_fields.go @@ -0,0 +1,242 @@ +package toml + +// Struct field handling is adapted from code in encoding/json: +// +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the Go distribution. + +import ( + "reflect" + "sort" + "sync" +) + +// A field represents a single field found in a struct. +type field struct { + name string // the name of the field (`toml` tag included) + tag bool // whether field has a `toml` tag + index []int // represents the depth of an anonymous field + typ reflect.Type // the type of the field +} + +// byName sorts field by name, breaking ties with depth, +// then breaking ties with "name came from toml tag", then +// breaking ties with index sequence. +type byName []field + +func (x byName) Len() int { return len(x) } + +func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x byName) Less(i, j int) bool { + if x[i].name != x[j].name { + return x[i].name < x[j].name + } + if len(x[i].index) != len(x[j].index) { + return len(x[i].index) < len(x[j].index) + } + if x[i].tag != x[j].tag { + return x[i].tag + } + return byIndex(x).Less(i, j) +} + +// byIndex sorts field by index sequence. +type byIndex []field + +func (x byIndex) Len() int { return len(x) } + +func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x byIndex) Less(i, j int) bool { + for k, xik := range x[i].index { + if k >= len(x[j].index) { + return false + } + if xik != x[j].index[k] { + return xik < x[j].index[k] + } + } + return len(x[i].index) < len(x[j].index) +} + +// typeFields returns a list of fields that TOML should recognize for the given +// type. The algorithm is breadth-first search over the set of structs to +// include - the top struct and then any reachable anonymous structs. +func typeFields(t reflect.Type) []field { + // Anonymous fields to explore at the current level and the next. + current := []field{} + next := []field{{typ: t}} + + // Count of queued names for current level and the next. + count := map[reflect.Type]int{} + nextCount := map[reflect.Type]int{} + + // Types already visited at an earlier level. + visited := map[reflect.Type]bool{} + + // Fields found. + var fields []field + + for len(next) > 0 { + current, next = next, current[:0] + count, nextCount = nextCount, map[reflect.Type]int{} + + for _, f := range current { + if visited[f.typ] { + continue + } + visited[f.typ] = true + + // Scan f.typ for fields to include. + for i := 0; i < f.typ.NumField(); i++ { + sf := f.typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { // unexported + continue + } + opts := getOptions(sf.Tag) + if opts.skip { + continue + } + index := make([]int, len(f.index)+1) + copy(index, f.index) + index[len(f.index)] = i + + ft := sf.Type + if ft.Name() == "" && ft.Kind() == reflect.Ptr { + // Follow pointer. + ft = ft.Elem() + } + + // Record found field and index sequence. + if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { + tagged := opts.name != "" + name := opts.name + if name == "" { + name = sf.Name + } + fields = append(fields, field{name, tagged, index, ft}) + if count[f.typ] > 1 { + // If there were multiple instances, add a second, + // so that the annihilation code will see a duplicate. + // It only cares about the distinction between 1 or 2, + // so don't bother generating any more copies. + fields = append(fields, fields[len(fields)-1]) + } + continue + } + + // Record new anonymous struct to explore in next round. + nextCount[ft]++ + if nextCount[ft] == 1 { + f := field{name: ft.Name(), index: index, typ: ft} + next = append(next, f) + } + } + } + } + + sort.Sort(byName(fields)) + + // Delete all fields that are hidden by the Go rules for embedded fields, + // except that fields with TOML tags are promoted. + + // The fields are sorted in primary order of name, secondary order + // of field index length. Loop over names; for each name, delete + // hidden fields by choosing the one dominant field that survives. + out := fields[:0] + for advance, i := 0, 0; i < len(fields); i += advance { + // One iteration per name. + // Find the sequence of fields with the name of this first field. + fi := fields[i] + name := fi.name + for advance = 1; i+advance < len(fields); advance++ { + fj := fields[i+advance] + if fj.name != name { + break + } + } + if advance == 1 { // Only one field with this name + out = append(out, fi) + continue + } + dominant, ok := dominantField(fields[i : i+advance]) + if ok { + out = append(out, dominant) + } + } + + fields = out + sort.Sort(byIndex(fields)) + + return fields +} + +// dominantField looks through the fields, all of which are known to +// have the same name, to find the single field that dominates the +// others using Go's embedding rules, modified by the presence of +// TOML tags. If there are multiple top-level fields, the boolean +// will be false: This condition is an error in Go and we skip all +// the fields. +func dominantField(fields []field) (field, bool) { + // The fields are sorted in increasing index-length order. The winner + // must therefore be one with the shortest index length. Drop all + // longer entries, which is easy: just truncate the slice. + length := len(fields[0].index) + tagged := -1 // Index of first tagged field. + for i, f := range fields { + if len(f.index) > length { + fields = fields[:i] + break + } + if f.tag { + if tagged >= 0 { + // Multiple tagged fields at the same level: conflict. + // Return no field. + return field{}, false + } + tagged = i + } + } + if tagged >= 0 { + return fields[tagged], true + } + // All remaining fields have the same length. If there's more than one, + // we have a conflict (two fields named "X" at the same level) and we + // return no field. + if len(fields) > 1 { + return field{}, false + } + return fields[0], true +} + +var fieldCache struct { + sync.RWMutex + m map[reflect.Type][]field +} + +// cachedTypeFields is like typeFields but uses a cache to avoid repeated work. +func cachedTypeFields(t reflect.Type) []field { + fieldCache.RLock() + f := fieldCache.m[t] + fieldCache.RUnlock() + if f != nil { + return f + } + + // Compute fields without lock. + // Might duplicate effort but won't hold other computations back. + f = typeFields(t) + if f == nil { + f = []field{} + } + + fieldCache.Lock() + if fieldCache.m == nil { + fieldCache.m = map[reflect.Type][]field{} + } + fieldCache.m[t] = f + fieldCache.Unlock() + return f +}