diff --git a/cmd/burrow/main.go b/cmd/burrow/main.go index fa38362061e30a01c4170f4d8b5081f7e9f1ccc7..6fb5ee54db1b4eb647c56175ee9283cd0e2de959 100644 --- a/cmd/burrow/main.go +++ b/cmd/burrow/main.go @@ -79,6 +79,10 @@ func main() { tomlOpt := cmd.BoolOpt("t toml", false, "Emit GenesisSpec as TOML rather than the "+ "default JSON") + baseOpt := cmd.StringsOpt("b base", nil, "Provide a base GenesisSpecs on top of which any "+ + "additional GenesisSpec presets specified by other flags will be merged. GenesisSpecs appearing "+ + "later take precedent over those appearing early if multiple --base flags are provided") + fullOpt := cmd.IntOpt("f full-accounts", 1, "Number of preset Full type accounts") validatorOpt := cmd.IntOpt("v validator-accounts", 0, "Number of preset Validator type accounts") rootOpt := cmd.IntOpt("r root-accounts", 0, "Number of preset Root type accounts") @@ -86,10 +90,19 @@ func main() { participantsOpt := cmd.IntOpt("p participant-accounts", 1, "Number of preset Participant type accounts") chainNameOpt := cmd.StringOpt("n chain-name", "", "Default chain name") - cmd.Spec = "[--full-accounts] [--validator-accounts] [--root-accounts] [--developer-accounts] [--participant-accounts] [--chain-name] [--toml]" + cmd.Spec = "[--base][--full-accounts] [--validator-accounts] [--root-accounts] [--developer-accounts] " + + "[--participant-accounts] [--chain-name] [--toml]" cmd.Action = func() { specs := make([]spec.GenesisSpec, 0, *participantsOpt+*fullOpt) + for _, baseSpec := range *baseOpt { + genesisSpec := new(spec.GenesisSpec) + err := source.FromFile(baseSpec, genesisSpec) + if err != nil { + fatalf("could not read GenesisSpec: %v", err) + } + specs = append(specs, *genesisSpec) + } for i := 0; i < *fullOpt; i++ { specs = append(specs, spec.FullAccount(i)) } @@ -106,7 +119,9 @@ func main() { specs = append(specs, spec.ParticipantAccount(i)) } genesisSpec := spec.MergeGenesisSpecs(specs...) - genesisSpec.ChainName = *chainNameOpt + if *chainNameOpt != "" { + genesisSpec.ChainName = *chainNameOpt + } if *tomlOpt { os.Stdout.WriteString(source.TOMLString(genesisSpec)) } else { @@ -121,18 +136,15 @@ func main() { genesisSpecOpt := cmd.StringOpt("s genesis-spec", "", "A GenesisSpec to use as a template for a GenesisDoc that will be created along with keys") - tomlInOpt := cmd.BoolOpt("t toml-in", false, "Consume GenesisSpec/GenesisDoc as TOML "+ - "rather than the JSON default") + jsonOutOpt := cmd.BoolOpt("j json-out", false, "Emit config in JSON rather than TOML "+ + "suitable for further processing or forming a separate genesis.json GenesisDoc") keysUrlOpt := cmd.StringOpt("k keys-url", "", fmt.Sprintf("Provide keys URL, default: %s", keys.DefaultKeysConfig().URL)) - jsonOutOpt := cmd.BoolOpt("j json-out", false, "Emit config in JSON rather than TOML "+ - "suitable for further processing or forming a separate genesis.json GenesisDoc") - - genesisDocOpt := cmd.StringOpt("g genesis-doc", "", "GenesisDoc JSON to embed in config") + genesisDocOpt := cmd.StringOpt("g genesis-doc", "", "GenesisDoc in JSON or TOML to embed in config") - separateGenesisDoc := cmd.StringOpt("s separate-genesis-doc", "", "Emit a separate genesis doc as JSON") + separateGenesisDoc := cmd.StringOpt("s separate-genesis-doc", "", "Emit a separate genesis doc as JSON or TOML") validatorIndexOpt := cmd.IntOpt("v validator-index", -1, "Validator index (in validators list - GenesisSpec or GenesisDoc) from which to set ValidatorAddress") @@ -151,8 +163,7 @@ func main() { chainNameOpt := cmd.StringOpt("n chain-name", "", "Default chain name") cmd.Spec = "[--keys-url=<keys URL>] [--genesis-spec=<GenesisSpec file> | --genesis-doc=<GenesisDoc file>] " + - "[--separate-genesis-doc=<genesis JSON file>]" + - "[--validator-index=<index>] [--chain-name] [--toml-in] [--json-out] " + + "[--separate-genesis-doc=<genesis JSON file>] [--validator-index=<index>] [--chain-name] [--json-out] " + "[--logging=<logging program>] [--describe-logging] [--debug]" cmd.Action = func() { @@ -160,7 +171,7 @@ func main() { if *configOpt != "" { // If explicitly given a config file use it as a base: - err := source.FromTOMLFile(*configOpt, conf) + err := source.FromFile(*configOpt, conf) if err != nil { fatalf("could not read base config file (as TOML): %v", err) } @@ -183,7 +194,7 @@ func main() { if *genesisSpecOpt != "" { genesisSpec := new(spec.GenesisSpec) - err := fromFile(*genesisSpecOpt, *tomlInOpt, genesisSpec) + err := source.FromFile(*genesisSpecOpt, genesisSpec) if err != nil { fatalf("could not read GenesisSpec: %v", err) } @@ -194,7 +205,7 @@ func main() { } } else if *genesisDocOpt != "" { genesisDoc := new(genesis.GenesisDoc) - err := fromFile(*genesisSpecOpt, *tomlInOpt, genesisDoc) + err := source.FromFile(*genesisSpecOpt, genesisDoc) if err != nil { fatalf("could not read GenesisSpec: %v", err) } @@ -288,10 +299,10 @@ func fatalf(format string, args ...interface{}) { func burrowConfigProvider(configFile string) source.ConfigProvider { return source.FirstOf( // Will fail if file doesn't exist, but still skipped it configFile == "" - source.TOMLFile(configFile, false), + source.File(configFile, false), source.Environment(config.DefaultBurrowConfigJSONEnvironmentVariable), // Try working directory - source.TOMLFile(config.DefaultBurrowConfigTOMLFileName, true), + source.File(config.DefaultBurrowConfigTOMLFileName, true), source.Default(config.DefaultBurrowConfig())) } @@ -308,7 +319,7 @@ func genesisDocProvider(genesisFile string, skipNonExistent bool) source.ConfigP "in config cascade, only specify GenesisDoc in one place") } genesisDoc := new(genesis.GenesisDoc) - err := source.FromJSONFile(genesisFile, genesisDoc) + err := source.FromFile(genesisFile, genesisDoc) if err != nil { return err } @@ -316,18 +327,3 @@ func genesisDocProvider(genesisFile string, skipNonExistent bool) source.ConfigP return nil }) } - -func fromFile(file string, toml bool, conf interface{}) (err error) { - if toml { - err = source.FromTOMLFile(file, conf) - if err != nil { - fatalf("could not read GenesisSpec: %v", err) - } - } else { - err = source.FromJSONFile(file, conf) - if err != nil { - fatalf("could not read GenesisSpec: %v", err) - } - } - return -} diff --git a/config/source/source.go b/config/source/source.go index b4a31d809bb6d150eaaaa7af1b2c17522fc39d45..16a1ad7830a6dfd0944a68a73e53f016f9990ce3 100644 --- a/config/source/source.go +++ b/config/source/source.go @@ -13,6 +13,7 @@ import ( "github.com/BurntSushi/toml" "github.com/cep21/xdgbasedir" "github.com/imdario/mergo" + "regexp" ) // If passed this identifier try to read config from STDIN @@ -31,6 +32,16 @@ type ConfigProvider interface { var _ ConfigProvider = &configSource{} +type Format string + +const ( + JSON Format = "JSON" + TOML Format = "TOML" + Unknown Format = "" +) + +var jsonRegex = regexp.MustCompile(`\s*{`) + type configSource struct { from string skip bool @@ -116,26 +127,14 @@ func EachOf(providers ...ConfigProvider) *configSource { return Cascade(os.Stderr, false, providers...) } -// Try to source config from provided JSON file, is skipNonExistent is true then the provider will fall-through (skip) -// when the file doesn't exist, rather than returning an error -func JSONFile(configFile string, skipNonExistent bool) *configSource { +// Try to source config from provided file detecting the file format, is skipNonExistent is true then the provider will +// fall-through (skip) when the file doesn't exist, rather than returning an error +func File(configFile string, skipNonExistent bool) *configSource { return &configSource{ skip: ShouldSkipFile(configFile, skipNonExistent), from: fmt.Sprintf("JSON config file at '%s'", configFile), apply: func(baseConfig interface{}) error { - return FromJSONFile(configFile, baseConfig) - }, - } -} - -// Try to source config from provided TOML file, is skipNonExistent is true then the provider will fall-through (skip) -// when the file doesn't exist, rather than returning an error -func TOMLFile(configFile string, skipNonExistent bool) *configSource { - return &configSource{ - skip: ShouldSkipFile(configFile, skipNonExistent), - from: fmt.Sprintf("TOML config file at '%s'", configFile), - apply: func(baseConfig interface{}) error { - return FromTOMLFile(configFile, baseConfig) + return FromFile(configFile, baseConfig) }, } } @@ -157,7 +156,7 @@ func XDGBaseDir(configFileName string) *configSource { if err != nil { return err } - return FromTOMLFile(configFile, baseConfig) + return FromFile(configFile, baseConfig) }, } } @@ -183,30 +182,39 @@ func Default(defaultConfig interface{}) *configSource { } } -func FromJSONFile(configFile string, conf interface{}) error { +func FromFile(configFile string, conf interface{}) error { bs, err := ReadFile(configFile) if err != nil { return err } - return FromJSONString(string(bs), conf) + return FromString(string(bs), conf) } -func FromTOMLFile(configFile string, conf interface{}) error { - bs, err := ReadFile(configFile) +func FromTOMLString(tomlString string, conf interface{}) error { + _, err := toml.Decode(tomlString, conf) if err != nil { return err } + return nil +} - return FromTOMLString(string(bs), conf) +func FromString(configString string, conf interface{}) error { + switch DetectFormat(configString) { + case JSON: + return FromJSONString(configString, conf) + case TOML: + return FromTOMLString(configString, conf) + default: + return fmt.Errorf("unknown configuration format:\n%s", configString) + } } -func FromTOMLString(tomlString string, conf interface{}) error { - _, err := toml.Decode(tomlString, conf) - if err != nil { - return err +func DetectFormat(configString string) Format { + if jsonRegex.MatchString(configString) { + return JSON } - return nil + return TOML } func FromJSONString(jsonString string, conf interface{}) error { diff --git a/config/source/source_test.go b/config/source/source_test.go index 7bf062b40dd6e1713725eb74da0177ce1d2ea861..08cc5424bbae5398ba8211df50a75e2c926247ae 100644 --- a/config/source/source_test.go +++ b/config/source/source_test.go @@ -31,7 +31,7 @@ func TestFile(t *testing.T) { file := writeConfigFile(t, newTestConfig()) defer os.Remove(file) conf := new(animalConfig) - err := TOMLFile(file, false).Apply(conf) + err := File(file, false).Apply(conf) assert.NoError(t, err) assert.Equal(t, tomlString, TOMLString(conf)) } @@ -42,7 +42,7 @@ func TestCascade(t *testing.T) { conf := newTestConfig() err := Cascade(os.Stderr, true, Environment(envVar), - TOMLFile("", false)).Apply(conf) + File("", false)).Apply(conf) assert.NoError(t, err) assert.Equal(t, newTestConfig(), conf) @@ -53,7 +53,7 @@ func TestCascade(t *testing.T) { conf = new(animalConfig) err = Cascade(os.Stderr, true, Environment(envVar), - TOMLFile(file, false)).Apply(conf) + File(file, false)).Apply(conf) assert.NoError(t, err) assert.Equal(t, TOMLString(fileConfig), TOMLString(conf)) @@ -66,11 +66,18 @@ func TestCascade(t *testing.T) { conf = newTestConfig() err = Cascade(os.Stderr, true, Environment(envVar), - TOMLFile(file, false)).Apply(conf) + File(file, false)).Apply(conf) assert.NoError(t, err) assert.Equal(t, TOMLString(envConfig), TOMLString(conf)) } +func TestDetectFormat(t *testing.T) { + assert.Equal(t, TOML, DetectFormat("")) + assert.Equal(t, JSON, DetectFormat("{")) + assert.Equal(t, JSON, DetectFormat("\n\n\t \n\n {")) + assert.Equal(t, TOML, DetectFormat("[Tendermint]\n Seeds =\"foobar@val0\"}")) +} + func writeConfigFile(t *testing.T, conf interface{}) string { tomlString := TOMLString(conf) f, err := ioutil.TempFile("", "source-test.toml")