diff --git a/go.mod b/go.mod index 370b992..a62707c 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/glamour v0.3.0 github.com/efficientgo/tools/core v0.0.0-20210609125236-d73259166f20 github.com/efficientgo/tools/extkingpin v0.0.0-20210609125236-d73259166f20 + github.com/fatih/structtag v1.2.0 github.com/felixge/fgprof v0.9.1 github.com/go-kit/kit v0.10.0 github.com/gobwas/glob v0.2.3 diff --git a/go.sum b/go.sum index 615b702..62d6603 100644 --- a/go.sum +++ b/go.sum @@ -193,6 +193,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/evanw/esbuild v0.6.5/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/fgprof v0.9.1 h1:E6FUJ2Mlv043ipLOCFqo8+cHo9MhQ203E2cdEK/isEs= github.com/felixge/fgprof v0.9.1/go.mod h1:7/HK6JFtFaARhIljgP2IV8rJLIoHDoOYoUphsnGvqxE= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= diff --git a/pkg/yamlgen/cfggen.go b/pkg/yamlgen/cfggen.go new file mode 100644 index 0000000..b87b05e --- /dev/null +++ b/pkg/yamlgen/cfggen.go @@ -0,0 +1,65 @@ +// Copyright (c) Bartłomiej Płotka @bwplotka +// Licensed under the Apache License 2.0. + +// Taken from Thanos project. +// +// Copyright (c) The Thanos Authors. +// Licensed under the Apache License 2.0. + +package yamlgen + +import ( + "io" + "reflect" + + "github.com/fatih/structtag" + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +func Generate(obj interface{}, w io.Writer) error { + // We forbid omitempty option. This is for simplification for doc generation. + if err := checkForOmitEmptyTagOption(obj); err != nil { + return errors.Wrap(err, "invalid type") + } + return yaml.NewEncoder(w).Encode(obj) +} + +func checkForOmitEmptyTagOption(obj interface{}) error { + return checkForOmitEmptyTagOptionRec(reflect.ValueOf(obj)) +} + +func checkForOmitEmptyTagOptionRec(v reflect.Value) error { + switch v.Kind() { + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + tags, err := structtag.Parse(string(v.Type().Field(i).Tag)) + if err != nil { + return errors.Wrapf(err, "%s: failed to parse tag %q", v.Type().Field(i).Name, v.Type().Field(i).Tag) + } + + tag, err := tags.Get("yaml") + if err != nil { + return errors.Wrapf(err, "%s: failed to get tag %q", v.Type().Field(i).Name, v.Type().Field(i).Tag) + } + + for _, opts := range tag.Options { + if opts == "omitempty" { + return errors.Errorf("omitempty is forbidden for config, but spotted on field '%s'", v.Type().Field(i).Name) + } + } + + if err := checkForOmitEmptyTagOptionRec(v.Field(i)); err != nil { + return errors.Wrapf(err, "%s", v.Type().Field(i).Name) + } + } + + case reflect.Ptr: + return errors.New("nil pointers are not allowed in configuration") + + case reflect.Interface: + return checkForOmitEmptyTagOptionRec(v.Elem()) + } + + return nil +}