pax_global_header00006660000000000000000000000064135251472010014512gustar00rootroot0000000000000052 comment=62a7af84b2eee3130fe1b42ef742a9609d7f38d1 kong-hcl-0.2.0/000077500000000000000000000000001352514720100132135ustar00rootroot00000000000000kong-hcl-0.2.0/.circleci/000077500000000000000000000000001352514720100150465ustar00rootroot00000000000000kong-hcl-0.2.0/.circleci/config.yml000066400000000000000000000015371352514720100170440ustar00rootroot00000000000000version: 2 jobs: build: environment: GO111MODULE: "on" GOBIN: "/go/bin" docker: - image: circleci/golang:1.11 working_directory: /go/src/github.com/alecthomas/kong-hcl steps: - checkout - run: name: Prepare command: | unset GOPATH go get -v github.com/jstemmer/go-junit-report curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s v1.15.0 mkdir ~/report when: always - run: name: Test command: | go test -v ./... 2>&1 | tee report.txt && /go/bin/go-junit-report < report.txt > ~/report/junit.xml - run: name: Lint command: | unset GOPATH ./bin/golangci-lint run - store_test_results: path: ~/report kong-hcl-0.2.0/.golangci.yml000066400000000000000000000015361352514720100156040ustar00rootroot00000000000000run: tests: true output: print-issued-lines: false linters: enable-all: true disable: - maligned - lll - gochecknoglobals linters-settings: govet: check-shadowing: true gocyclo: min-complexity: 10 dupl: threshold: 100 goconst: min-len: 5 min-occurrences: 3 gocyclo: min-complexity: 20 issues: max-per-linter: 0 max-same: 0 exclude-use-default: false exclude: - '^(G104|G204):' # Very commonly not checked. - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked' - 'exported method (.*\.MarshalJSON|.*\.UnmarshalJSON) should have comment or be unexported' - 'composite literal uses unkeyed fields' - 'bad syntax for struct tag pair' - 'bad syntax for struct tag key' kong-hcl-0.2.0/COPYING000066400000000000000000000020371352514720100142500ustar00rootroot00000000000000Copyright (C) 2019 Alec Thomas 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. kong-hcl-0.2.0/README.md000066400000000000000000000033151352514720100144740ustar00rootroot00000000000000# A Kong configuration loader for HCL [![](https://godoc.org/github.com/alecthomas/kong-hcl?status.svg)](http://godoc.org/github.com/alecthomas/kong-hcl) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/kong-hcl.svg)](https://circleci.com/gh/alecthomas/kong-hcl) Use it like so: ```go var cli struct { Config kong.ConfigFlag `help:"Load configuration."` } parser, err := kong.New(&cli, kong.Configuration(konghcl.Loader, "/etc/myapp/config.hcl", "~/.myapp.hcl)) ``` ## Mapping HCL fragments to a struct More complex structures can be loaded directly into flag values by implementing the `kong.MapperValue` interface, and calling `konghcl.DecodeValue`. The value can either be a HCL(/JSON) fragment, or a path to a HCL file that will be loaded. Both can be specified on the command-line or config file. eg. ```go type NestedConfig struct { Size int Name string } type ComplexConfig struct { Key bool Nested map[string]NestedConfig } func (c *ComplexConfig) Decode(ctx *kong.DecodeContext) error { return konghcl.DecodeValue(ctx, c) } // ... type Config struct { Complex ComplexConfig } ``` Then the following `.hcl` config fragment will be decoded into `Complex`: ```hcl complex { key = true nested first { size = 10 name = "first name" } nested second { size = 12 name = "second name" } } ``` ## Configuration layout Configuration keys are mapped directly to flags. Additionally, HCL block keys will be used as a hyphen-separated prefix when looking up flags. ## Example The following HCL configuration file... ```hcl debug = true db { dsn = "root@/database" trace = true } ``` Maps to the following flags: ``` --debug --db-dsn= --db-trace ``` kong-hcl-0.2.0/dump.go000066400000000000000000000041431352514720100145110ustar00rootroot00000000000000package konghcl import ( "fmt" "sort" "strings" "github.com/alecthomas/kong" ) var ( // DumpIgnoreFlags specifies a set of flags that should not be dumped. DumpIgnoreFlags = map[string]bool{ "help": true, "version": true, "dump-config": true, "env": true, "validate-config": true, } ) // DumpConfig can be added as a flag to dump HCL configuration. type DumpConfig bool func (f DumpConfig) BeforeApply(app *kong.Kong) error { // nolint: golint groups := map[string][]*kong.Flag{} standalone := []*kong.Flag{} for _, flags := range app.Model.AllFlags(true) { for _, flag := range flags { if DumpIgnoreFlags[flag.Name] { continue } parts := strings.SplitN(flag.Name, "-", 2) if len(parts) == 1 { standalone = append(standalone, flag) } else { groups[parts[0]] = append(groups[parts[0]], flag) } } } // Write non-grouped flags out at the top. for key, flags := range groups { if len(flags) == 1 { standalone = append(standalone, flags...) delete(groups, key) } } // Alphabetical ordering. sort.Slice(standalone, func(i, j int) bool { return standalone[i].Name < standalone[j].Name }) for _, flag := range standalone { formatFlag("", flag, false) fmt.Println() } delete(groups, "") // Alphabetically order the groups. keys := []string{} for key := range groups { keys = append(keys, key) } sort.Strings(keys) for block := range groups { flags := groups[block] if len(flags) == 1 { formatFlag("", flags[0], false) fmt.Println() continue } fmt.Printf("%s {\n", block) for i, flag := range flags { if i != 0 { fmt.Println() } formatFlag(" ", flag, true) } fmt.Printf("}\n\n") } app.Exit(0) return nil } func formatFlag(indent string, flag *kong.Flag, grouped bool) { fmt.Printf("%s// %s\n", indent, flag.Help) fmt.Print(indent) if grouped { parts := strings.SplitN(flag.Name, "-", 2) fmt.Printf("%s = ", parts[1]) } else { fmt.Printf("%s = ", flag.Name) } switch { case flag.IsSlice(): fmt.Println("[ ... ]") case flag.IsMap(): fmt.Println("{ ... }") default: fmt.Println(flag.FormatPlaceHolder()) } } kong-hcl-0.2.0/go.mod000066400000000000000000000003271352514720100143230ustar00rootroot00000000000000module github.com/alecthomas/kong-hcl require ( github.com/alecthomas/kong v0.2.1-0.20190721020729-f7d3d9bfb5ed github.com/hashicorp/hcl v1.0.0 github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.2.2 ) kong-hcl-0.2.0/go.sum000066400000000000000000000023611352514720100143500ustar00rootroot00000000000000github.com/alecthomas/kong v0.2.1-0.20190721020729-f7d3d9bfb5ed h1:tKInwxmkm0591VqMjm5+3YJuqZF61QIcxydxSpyOdrQ= github.com/alecthomas/kong v0.2.1-0.20190721020729-f7d3d9bfb5ed/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= kong-hcl-0.2.0/loader.go000066400000000000000000000115401352514720100150110ustar00rootroot00000000000000package konghcl import ( "encoding/json" "fmt" "io" "io/ioutil" "os" "strings" "github.com/alecthomas/kong" "github.com/hashicorp/hcl" "github.com/pkg/errors" ) // Resolver resolves kong Flags from configuration in HCL. type Resolver struct { config map[string]interface{} } var _ kong.ConfigurationLoader = Loader // DecodeValue decodes Kong values into a Go structure. func DecodeValue(ctx *kong.DecodeContext, dest interface{}) error { v := ctx.Scan.Pop().Value var ( data []byte err error ) switch v := v.(type) { case string: // Value is a string; it can either be a filename or a HCL fragment. filename := kong.ExpandPath(v) data, err = ioutil.ReadFile(filename) // nolint: gosec if os.IsNotExist(err) { data = []byte(v) } else if err != nil { return errors.Wrapf(err, "invalid HCL in %q", filename) } case []map[string]interface{}: merged := map[string]interface{}{} for _, m := range v { for k, v := range m { merged[k] = v } } data, err = json.Marshal(merged) if err != nil { return err } default: data, err = json.Marshal(v) if err != nil { return err } } return errors.Wrapf(hcl.Unmarshal(data, dest), "invalid HCL %q", data) } // Loader is a Kong configuration loader for HCL. func Loader(r io.Reader) (kong.Resolver, error) { data, err := ioutil.ReadAll(r) if err != nil { return nil, err } config := map[string]interface{}{} err = hcl.Unmarshal(data, &config) if err != nil { return nil, errors.Wrap(err, "invalid HCL") } return &Resolver{config: config}, nil } func (r *Resolver) Validate(app *kong.Application) error { // nolint: golint // Find all valid configuration keys from the Application. valid := map[string]bool{} rawPrefixes := []string{} path := []string{} _ = kong.Visit(app, func(node kong.Visitable, next kong.Next) error { switch node := node.(type) { case *kong.Node: path = append(path, node.Name) _ = next(nil) path = path[:len(path)-1] return nil case *kong.Flag: flagPath := append([]string{}, path...) if node.Group != "" { flagPath = append(flagPath, node.Group) } key := strings.Join(append(flagPath, node.Name), "-") if _, ok := node.Target.Interface().(kong.MapperValue); ok { rawPrefixes = append(rawPrefixes, key) } else { valid[key] = true } default: return next(nil) } return nil }) // Then check all configuration keys against the Application keys. next: for key := range flattenConfig(valid, r.config) { if !valid[key] { for _, prefix := range rawPrefixes { if strings.HasPrefix(key, prefix) { continue next } } return errors.Errorf("unknown configuration key %q", key) } } return nil } func (r *Resolver) Resolve(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { // nolint: golint path := r.pathForFlag(parent, flag) return find(r.config, path) } // Build a string path up to this flag. func (r *Resolver) pathForFlag(parent *kong.Path, flag *kong.Flag) []string { path := []string{} for n := parent.Node(); n != nil && n.Type != kong.ApplicationNode; n = n.Parent { path = append([]string{n.Name}, path...) } if flag.Group != "" { path = append([]string{flag.Group}, path...) } path = append(path, flag.Name) return path } // Find the value that path maps to. func find(config map[string]interface{}, path []string) (interface{}, error) { if len(path) == 0 { return config, nil } key := strings.Join(path, "-") parts := strings.SplitN(key, "-", -1) for i := len(parts); i > 0; i-- { prefix := strings.Join(parts[:i], "-") if sub := config[prefix]; sub != nil { if sub, ok := sub.([]map[string]interface{}); ok { if len(sub) > 1 { return sub, nil } return find(sub[0], parts[i:]) } return sub, nil } } return nil, nil } func flattenConfig(schema map[string]bool, config map[string]interface{}) map[string]bool { out := map[string]bool{} next: for _, path := range flattenNode(config) { for i := len(path) - 1; i >= 0; i-- { candidate := strings.Join(path[:i], "-") if schema[candidate] { out[candidate] = true continue next } } out[strings.Join(path, "-")] = true } return out } func flattenNode(config interface{}) [][]string { out := [][]string{} switch config := config.(type) { case []map[string]interface{}: for _, group := range config { out = append(out, flattenNode(group)...) } case map[string]interface{}: for key, value := range config { children := flattenNode(value) if len(children) == 0 { out = append(out, []string{key}) } else { for _, childValue := range children { out = append(out, append([]string{key}, childValue...)) } } } case []interface{}: for _, el := range config { out = flattenNode(el) } case bool, float64, int, string: return nil default: panic(fmt.Sprintf("unsupported type %T", config)) } return out } kong-hcl-0.2.0/loader_test.go000066400000000000000000000062721352514720100160560ustar00rootroot00000000000000// nolint: govet package konghcl import ( "io/ioutil" "os" "strings" "testing" "github.com/alecthomas/kong" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testConfig = ` flag-name = "hello world" int-flag = 10 float-flag = 10.5 slice-flag = [1, 2, 3] prefix { prefixed-flag = "prefixed flag" } group { grouped-flag = "grouped flag" embedded-flag = "embedded flag" } map-flag = { key = "value" } // Multiple keys are merged. mapped = { left = "left" } mapped = { right = "right" } prefix-block { embedded-flag = "yes" } ` type mapperValue struct { Left string Right string } func (m *mapperValue) Decode(ctx *kong.DecodeContext) error { return DecodeValue(ctx, m) } func TestHCL(t *testing.T) { type Embedded struct { EmbeddedFlag string } type CLI struct { FlagName string IntFlag int FloatFlag float64 SliceFlag []int GroupedFlag string `group:"group"` PrefixedFlag string `prefix:"prefix-"` Embedded `group:"group"` PrefixedBlock Embedded `embed:"" prefix:"prefix-block-"` MapFlag map[string]string Mapped mapperValue } t.Run("FromResolver", func(t *testing.T) { var cli CLI r := strings.NewReader(testConfig) resolver, err := Loader(r) require.NoError(t, err) parser, err := kong.New(&cli, kong.Resolvers(resolver)) require.NoError(t, err) _, err = parser.Parse(nil) require.NoError(t, err) assert.Equal(t, "hello world", cli.FlagName) assert.Equal(t, "grouped flag", cli.GroupedFlag) assert.Equal(t, "prefixed flag", cli.PrefixedFlag) assert.Equal(t, "embedded flag", cli.EmbeddedFlag) assert.Equal(t, 10, cli.IntFlag) assert.Equal(t, 10.5, cli.FloatFlag) assert.Equal(t, []int{1, 2, 3}, cli.SliceFlag) assert.Equal(t, map[string]string{"key": "value"}, cli.MapFlag) assert.Equal(t, mapperValue{Left: "left", Right: "right"}, cli.Mapped) assert.Equal(t, Embedded{EmbeddedFlag: "yes"}, cli.PrefixedBlock) }) t.Run("FragmentFromFlag", func(t *testing.T) { var cli CLI parser, err := kong.New(&cli) require.NoError(t, err) _, err = parser.Parse([]string{"--mapped", ` left = "LEFT" right = "RIGHT" `}) require.NoError(t, err) require.Equal(t, mapperValue{Left: "LEFT", Right: "RIGHT"}, cli.Mapped) }) t.Run("FragmentFromFile", func(t *testing.T) { w, err := ioutil.TempFile("", "kong-hcl-") require.NoError(t, err) _, err = w.Write([]byte(` left = "LEFT" right = "RIGHT" `)) require.NoError(t, err) _ = w.Close() defer os.Remove(w.Name()) var cli CLI parser, err := kong.New(&cli) require.NoError(t, err) _, err = parser.Parse([]string{"--mapped", w.Name()}) require.NoError(t, err) require.Equal(t, mapperValue{Left: "LEFT", Right: "RIGHT"}, cli.Mapped) }) } func TestHCLValidation(t *testing.T) { type command struct { CommandFlag string } var cli struct { Command command `cmd:""` Flag string } resolver, err := Loader(strings.NewReader(` invalid-flag = true `)) require.NoError(t, err) parser, err := kong.New(&cli, kong.Resolvers(resolver)) require.NoError(t, err) _, err = parser.Parse([]string{"command"}) require.EqualError(t, err, "unknown configuration key \"invalid-flag\"") }