diff --git a/README.md b/README.md
index 03b016e..2da25a5 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,8 @@ type Config struct {
* `-all` - Generate documentation for all types in the file.
* `-env-prefix` - Environment variable prefix.
* `-no-styles` - Disable built-int CSS styles for HTML format.
+ * `-field-names` - Use field names instead of struct tags for variable names
+ if tags are not set.
## Example
diff --git a/_examples/complex-fields.md b/_examples/complex-fields.md
new file mode 100644
index 0000000..3ba3779
--- /dev/null
+++ b/_examples/complex-fields.md
@@ -0,0 +1,12 @@
+# Environment Variables
+
+## FieldNames
+
+FieldNames uses field names as env names.
+
+ - `FOO` - Foo is a single field.
+ - `BAR` - Bar and Baz are two fields.
+ - `BAZ` - Bar and Baz are two fields.
+ - `QUUX` - Quux is a field with a tag.
+ - `FOO_BAR` (default: `quuux`) - FooBar is a field with a default value.
+ - `REQUIRED` (**required**) - Required is a required field.
diff --git a/_examples/complex-nostyle.html b/_examples/complex-nostyle.html
index 96e439a..f8ab3cf 100644
--- a/_examples/complex-nostyle.html
+++ b/_examples/complex-nostyle.html
@@ -31,6 +31,13 @@
NextConfig
MOUNT
(required) - Mount is a mount point.
+
+
+ FieldNames
+FieldNames uses field names as env names.
+
+ QUUX
- Quux is a field with a tag.
+
diff --git a/_examples/complex.go b/_examples/complex.go
index fda57e9..eec1ae1 100644
--- a/_examples/complex.go
+++ b/_examples/complex.go
@@ -33,3 +33,19 @@ type NextConfig struct { // NextConfig is a configuration structure.
// Mount is a mount point.
Mount string `env:"MOUNT,required"`
}
+
+// FieldNames uses field names as env names.
+//
+//go:generate go run ../ -output complex-fields.md -field-names
+type FieldNames struct {
+ // Foo is a single field.
+ Foo string
+ // Bar and Baz are two fields.
+ Bar, Baz string
+ // Quux is a field with a tag.
+ Quux string `env:"QUUX"`
+ // FooBar is a field with a default value.
+ FooBar string `envDefault:"quuux"`
+ // Required is a required field.
+ Required string `env:",required"`
+}
diff --git a/_examples/complex.html b/_examples/complex.html
index 4632d14..8d1268d 100644
--- a/_examples/complex.html
+++ b/_examples/complex.html
@@ -97,6 +97,13 @@ NextConfig
MOUNT
(required) - Mount is a mount point.
+
+
+ FieldNames
+FieldNames uses field names as env names.
+
+ QUUX
- Quux is a field with a tag.
+
diff --git a/_examples/complex.md b/_examples/complex.md
index 64bed6d..9fcbb8a 100644
--- a/_examples/complex.md
+++ b/_examples/complex.md
@@ -18,3 +18,9 @@ It is trying to cover all the possible cases.
## NextConfig
- `MOUNT` (**required**) - Mount is a mount point.
+
+## FieldNames
+
+FieldNames uses field names as env names.
+
+ - `QUUX` - Quux is a field with a tag.
diff --git a/_examples/complex.txt b/_examples/complex.txt
index 7d322f7..1891eb5 100644
--- a/_examples/complex.txt
+++ b/_examples/complex.txt
@@ -18,3 +18,9 @@ It is trying to cover all the possible cases.
## NextConfig
* `MOUNT` (required) - Mount is a mount point.
+
+## FieldNames
+
+FieldNames uses field names as env names.
+
+ * `QUUX` - Quux is a field with a tag.
diff --git a/_examples/x_complex.md b/_examples/x_complex.md
index 8abc90d..39fd51e 100644
--- a/_examples/x_complex.md
+++ b/_examples/x_complex.md
@@ -18,3 +18,9 @@ It is trying to cover all the possible cases.
## NextConfig
- `X_MOUNT` (**required**) - Mount is a mount point.
+
+## FieldNames
+
+FieldNames uses field names as env names.
+
+ - `X_QUUX` - Quux is a field with a tag.
diff --git a/doc.go b/doc.go
index 760a417..32eecbf 100644
--- a/doc.go
+++ b/doc.go
@@ -37,5 +37,7 @@ Options:
- `-all` - Generate documentation for all types in the file.
- `-env-prefix` - Environment variable prefix.
- `-no-styles` - Disable built-int CSS styles for HTML format.
+ - `-field-names` - Use field names instead of struct tags for variable names
+ if tags are not set.
*/
package main
diff --git a/generator.go b/generator.go
index e17def8..af11b96 100644
--- a/generator.go
+++ b/generator.go
@@ -13,6 +13,7 @@ type generator struct {
tmpl template
prefix string
noStyles bool
+ fieldNames bool
}
type generatorOption func(*generator) error
@@ -65,6 +66,13 @@ func withNoStyles() generatorOption {
}
}
+func withFieldNames() generatorOption {
+ return func(g *generator) error {
+ g.fieldNames = true
+ return nil
+ }
+}
+
func newGenerator(fileName string, execLine int, opts ...generatorOption) (*generator, error) {
g := &generator{fileName: fileName, execLine: execLine}
for _, opt := range opts {
@@ -79,7 +87,7 @@ func newGenerator(fileName string, execLine int, opts ...generatorOption) (*gene
}
func (g *generator) generate(out io.Writer) error {
- insp := newInspector(g.targetType, g.all, g.execLine)
+ insp := newInspector(g.targetType, g.all, g.execLine, g.fieldNames)
data, err := insp.inspectFile(g.fileName)
if err != nil {
return fmt.Errorf("inspect file: %w", err)
diff --git a/generator_test.go b/generator_test.go
index 3ba8072..fb9de93 100644
--- a/generator_test.go
+++ b/generator_test.go
@@ -61,6 +61,15 @@ func TestOptions(t *testing.T) {
t.Fatal("expected noStyles to be true")
}
})
+ t.Run("WithFieldNames", func(t *testing.T) {
+ g, err := newGenerator("stub", 1, withFieldNames())
+ if err != nil {
+ t.Fatal("new generator error", err)
+ }
+ if g.fieldNames != true {
+ t.Fatal("expected fieldNames to be true")
+ }
+ })
t.Run("empty", func(t *testing.T) {
g, err := newGenerator("stub", 1)
if err != nil {
diff --git a/inspector.go b/inspector.go
index ffcd07d..8a5ad02 100644
--- a/inspector.go
+++ b/inspector.go
@@ -10,9 +10,10 @@ import (
)
type inspector struct {
- typeName string // type name to generate documentation for, could be empty
- all bool // generate documentation for all types in the file
- execLine int // line number of the go:generate directive
+ typeName string // type name to generate documentation for, could be empty
+ all bool // generate documentation for all types in the file
+ execLine int // line number of the go:generate directive
+ useFieldNames bool // use field names if tag is not specified
fileSet *token.FileSet
lines []int
@@ -22,8 +23,8 @@ type inspector struct {
err error
}
-func newInspector(typeName string, all bool, execLine int) *inspector {
- return &inspector{typeName: typeName, all: all, execLine: execLine}
+func newInspector(typeName string, all bool, execLine int, useFieldNames bool) *inspector {
+ return &inspector{typeName: typeName, all: all, execLine: execLine, useFieldNames: useFieldNames}
}
func (i *inspector) inspectFile(fileName string) ([]*EnvScope, error) {
@@ -44,19 +45,17 @@ func (i *inspector) inspect(node ast.Node) ([]*EnvScope, error) {
return i.items, i.err
}
-func (i *inspector) getScope(t *ast.TypeSpec) (out *EnvScope) {
+func (i *inspector) getScope(t *ast.TypeSpec) *EnvScope {
typeName := t.Name.Name
for _, s := range i.items {
if s.typeName == typeName {
- out = s
- return
+ return s
}
}
- out = new(EnvScope)
- parseType(t, i.doc, out)
- i.items = append(i.items, out)
- return
+ s := i.parseType(t)
+ i.items = append(i.items, s)
+ return s
}
func (i *inspector) Visit(n ast.Node) ast.Visitor {
@@ -114,11 +113,11 @@ func (i *inspector) Visit(n ast.Node) ast.Visitor {
if st, ok := t.Type.(*ast.StructType); ok {
scope := i.getScope(t)
for _, field := range st.Fields.List {
- var item EnvDocItem
- if !parseField(field, &item) {
+ items := i.parseField(field)
+ if len(items) == 0 {
continue
}
- scope.Vars = append(scope.Vars, item)
+ scope.Vars = append(scope.Vars, items...)
}
}
// reset pending type flag event if this type
@@ -128,19 +127,22 @@ func (i *inspector) Visit(n ast.Node) ast.Visitor {
return i
}
-func parseType(t *ast.TypeSpec, doc *doc.Package, out *EnvScope) {
+func (i *inspector) parseType(t *ast.TypeSpec) *EnvScope {
typeName := t.Name.Name
- out.Doc = strings.TrimSpace(t.Doc.Text())
- if out.Doc == "" {
- for _, t := range doc.Types {
+ docStr := strings.TrimSpace(t.Doc.Text())
+ if docStr == "" {
+ for _, t := range i.doc.Types {
if t.Name == typeName {
- out.Doc = strings.TrimSpace(t.Doc)
+ docStr = strings.TrimSpace(t.Doc)
break
}
}
}
- out.Name = typeName
- out.typeName = typeName
+ return &EnvScope{
+ Name: typeName,
+ Doc: docStr,
+ typeName: typeName,
+ }
}
func getTagValues(tag, tagName string) []string {
@@ -161,40 +163,54 @@ func getTagValues(tag, tagName string) []string {
return strings.Split(tagValue, ",")
}
-func parseField(f *ast.Field, out *EnvDocItem) bool {
- if f.Tag == nil {
- return false
+func (i *inspector) parseField(f *ast.Field) (out []EnvDocItem) {
+ if f.Tag == nil && !i.useFieldNames {
+ return
}
- tag := f.Tag.Value
- if !strings.Contains(tag, "env:") {
- return false
+ var tag string
+ if t := f.Tag; t != nil {
+ tag = t.Value
+ }
+ if !strings.Contains(tag, "env:") && !i.useFieldNames {
+ return
}
tagValues := getTagValues(tag, "env")
- if len(tagValues) == 0 {
- return false
+ if len(tagValues) > 0 && tagValues[0] != "" {
+ var item EnvDocItem
+ item.Name = tagValues[0]
+ out = []EnvDocItem{item}
+ } else if i.useFieldNames {
+ out = make([]EnvDocItem, len(f.Names))
+ for i, name := range f.Names {
+ out[i].Name = camelToSnake(name.Name)
+ }
+ } else {
+ return
}
- out.Name = tagValues[0]
- if f.Doc != nil {
- out.Doc = strings.TrimSpace(f.Doc.Text())
+ docStr := strings.TrimSpace(f.Doc.Text())
+ if docStr == "" {
+ docStr = strings.TrimSpace(f.Comment.Text())
}
- if out.Doc == "" && f.Comment != nil {
- out.Doc = strings.TrimSpace(f.Comment.Text())
+ for i := range out {
+ out[i].Doc = docStr
}
var opts EnvVarOptions
- for _, tagValue := range tagValues[1:] {
- switch tagValue {
- case "required":
- opts.Required = true
- case "expand":
- opts.Expand = true
- case "notEmpty":
- opts.Required = true
- opts.NonEmpty = true
- case "file":
- opts.FromFile = true
+ if len(tagValues) > 1 {
+ for _, tagValue := range tagValues[1:] {
+ switch tagValue {
+ case "required":
+ opts.Required = true
+ case "expand":
+ opts.Expand = true
+ case "notEmpty":
+ opts.Required = true
+ opts.NonEmpty = true
+ case "file":
+ opts.FromFile = true
+ }
}
}
@@ -212,7 +228,8 @@ func parseField(f *ast.Field, out *EnvDocItem) bool {
opts.Separator = ","
}
- out.Opts = opts
-
- return true
+ for i := range out {
+ out[i].Opts = opts
+ }
+ return
}
diff --git a/inspector_test.go b/inspector_test.go
index 7804cea..7ee4320 100644
--- a/inspector_test.go
+++ b/inspector_test.go
@@ -2,6 +2,7 @@ package main
import (
"embed"
+ "errors"
"fmt"
"go/ast"
"io"
@@ -12,9 +13,12 @@ import (
func TestTagParsers(t *testing.T) {
type testCase struct {
- tag string
- expect EnvDocItem
- fail bool
+ tag string
+ names []string
+ useFieldNames bool
+ expect EnvDocItem
+ expectList []EnvDocItem
+ fail bool
}
for i, c := range []testCase{
{tag: "", fail: true},
@@ -60,24 +64,86 @@ func TestTagParsers(t *testing.T) {
Opts: EnvVarOptions{Separator: ";"},
},
},
+ {
+ names: []string{"Foo", "BarBaz"},
+ expectList: []EnvDocItem{
+ {Name: "FOO"},
+ {Name: "BAR_BAZ"},
+ },
+ useFieldNames: true,
+ },
+ {
+ names: []string{"Foo"},
+ tag: `env:",required"`,
+ expectList: []EnvDocItem{
+ {Name: "FOO", Opts: EnvVarOptions{Required: true}},
+ },
+ useFieldNames: true,
+ },
} {
t.Run(fmt.Sprint(i), func(t *testing.T) {
- var out EnvDocItem
+ fieldNames := make([]*ast.Ident, len(c.names))
+ for i, name := range c.names {
+ fieldNames[i] = &ast.Ident{Name: name}
+ }
+ var tag *ast.BasicLit
+ if c.tag != "" {
+ tag = &ast.BasicLit{Value: c.tag}
+ }
field := &ast.Field{
- Tag: &ast.BasicLit{Value: c.tag},
+ Tag: tag,
+ Names: fieldNames,
}
- ok := parseField(field, &out)
- if ok != !c.fail {
- t.Error("parseTag returned false")
+ i := inspector{
+ useFieldNames: c.useFieldNames,
}
- if out != c.expect {
- t.Errorf("parseTag of `%s` returned wrong result: %+v; expected: %+v", c.tag, out, c.expect)
+
+ expect := c.expectList
+ if len(expect) == 0 && c.expect.Name != "" {
+ expect = []EnvDocItem{c.expect}
+ }
+
+ actual := i.parseField(field)
+ if c.fail {
+ if actual != nil {
+ t.Errorf("expected nil, got %#v", actual)
+ }
+ return
+ }
+ if len(expect) != len(actual) {
+ t.Errorf("expected %d items, got %d", len(expect), len(actual))
+ }
+ for i, e := range expect {
+ a := actual[i]
+ if e.Name != a.Name {
+ t.Errorf("expected[%d] name %q, got %q", i, e.Name, a.Name)
+ }
+ if e.Doc != a.Doc {
+ t.Errorf("expected[%d] doc %q, got %q", i, e.Doc, a.Doc)
+ }
+ if e.Opts != a.Opts {
+ t.Errorf("expected[%d] opts %#v, got %#v", i, e.Opts, a.Opts)
+ }
}
})
}
}
+func TestInspectorError(t *testing.T) {
+ sourceFile := path.Join(t.TempDir(), "tmp.go")
+ if err := copyTestFile(path.Join("testdata", "type.go"), sourceFile); err != nil {
+ t.Fatal("Copy test file data", err)
+ }
+ insp := newInspector("", true, 0, false)
+ targetErr := errors.New("target error")
+ insp.err = targetErr
+ _, err := insp.inspectFile(sourceFile)
+ if err != targetErr {
+ t.Errorf("Expected error %q, got %q", targetErr, err)
+ }
+}
+
//go:embed testdata
var testdata embed.FS
@@ -252,7 +318,7 @@ func inspectorTester(name string, typeName string, all bool, lineN int, expect [
if err := copyTestFile(path.Join("testdata", name), sourceFile); err != nil {
t.Fatal("Copy test file data", err)
}
- insp := newInspector(typeName, all, lineN)
+ insp := newInspector(typeName, all, lineN, false)
scopes, err := insp.inspectFile(sourceFile)
if err != nil {
t.Fatal("Inspector failed", err)
diff --git a/main.go b/main.go
index 77bbb46..3d53aae 100644
--- a/main.go
+++ b/main.go
@@ -16,6 +16,7 @@ type appConfig struct {
all bool
envPrefix string
noStyles bool
+ fieldNames bool
}
func (cfg *appConfig) parseFlags(f *flag.FlagSet) error {
@@ -25,6 +26,7 @@ func (cfg *appConfig) parseFlags(f *flag.FlagSet) error {
f.BoolVar(&cfg.all, "all", false, "Generate documentation for all types in the file")
f.StringVar(&cfg.envPrefix, "env-prefix", "", "Environment variable prefix")
f.BoolVar(&cfg.noStyles, "no-styles", false, "Disable styles in html output")
+ f.BoolVar(&cfg.fieldNames, "field-names", false, "Use field names if tag is not specified")
if err := f.Parse(os.Args[1:]); err != nil {
return fmt.Errorf("parsing CLI args: %w", err)
}
@@ -104,6 +106,9 @@ func run(cfg *appConfig) (err error) {
if cfg.noStyles {
opts = append(opts, withNoStyles())
}
+ if cfg.fieldNames {
+ opts = append(opts, withFieldNames())
+ }
gen, err := newGenerator(cfg.inputFileName, cfg.execLine, opts...)
if err != nil {
return fmt.Errorf("creating generator: %w", err)
diff --git a/main_test.go b/main_test.go
index 2aeb788..6cc0827 100644
--- a/main_test.go
+++ b/main_test.go
@@ -16,6 +16,7 @@ func TestConfig(t *testing.T) {
"-no-styles",
"-format", "markdown",
"-env-prefix", "TEST_",
+ "-field-names",
"-all",
}
t.Setenv("GOFILE", "test.go")
@@ -43,6 +44,9 @@ func TestConfig(t *testing.T) {
if !cfg.noStyles {
t.Fatal("Invalid no styles flag")
}
+ if !cfg.fieldNames {
+ t.Fatal("Invalid field names flag")
+ }
if err := cfg.parseEnv(); err != nil {
t.Fatal("Invalid environment:", err)
@@ -129,9 +133,31 @@ func TestMainRun(t *testing.T) {
inputFileName: inputFile,
execLine: 0,
envPrefix: "TEST_",
+ noStyles: true,
+ fieldNames: true,
+ all: true,
}
if err := run(&config); err != nil {
t.Fatal("run", err)
}
})
+ t.Run("bad-out", func(t *testing.T) {
+ inputFile := path.Join(t.TempDir(), "example.go")
+ if err := copyTestFile(path.Join("testdata", "type.go"), inputFile); err != nil {
+ t.Fatal("copy test file", err)
+ }
+ config := appConfig{
+ typeName: "Type1",
+ formatName: "markdown",
+ outputFileName: "",
+ inputFileName: inputFile,
+ execLine: 0,
+ envPrefix: "TEST_",
+ }
+ err := run(&config)
+ if err == nil {
+ t.Fatal("Expect error for invalid output file name")
+ }
+ t.Logf("Got error as expected: %v", err)
+ })
}
diff --git a/utils.go b/utils.go
index e8c28d3..d28df46 100644
--- a/utils.go
+++ b/utils.go
@@ -1,6 +1,10 @@
package main
-import "io"
+import (
+ "io"
+ "strings"
+ "unicode"
+)
func closeWith(closer io.Closer, handler func(error)) {
err := closer.Close()
@@ -8,3 +12,20 @@ func closeWith(closer io.Closer, handler func(error)) {
handler(err)
}
}
+
+func camelToSnake(s string) string {
+ const underscore = '_'
+ var result strings.Builder
+ result.Grow(len(s) + 5)
+
+ var prev rune
+ for i, r := range s {
+ if i > 0 && prev != underscore && r != underscore && unicode.IsUpper(r) {
+ result.WriteRune(underscore)
+ }
+ result.WriteRune(unicode.ToUpper(r))
+ prev = r
+ }
+
+ return result.String()
+}
diff --git a/utils_test.go b/utils_test.go
index d36e035..fede6b5 100644
--- a/utils_test.go
+++ b/utils_test.go
@@ -32,3 +32,23 @@ func TestCloseWith(t *testing.T) {
t.Fatalf("expected handler to be called with %q, got %q", targetErr, handlerErr)
}
}
+
+func TestCamelToSnake(t *testing.T) {
+ type testCase [2]string
+ for _, tc := range []testCase{
+ {"Foo", "FOO"},
+ {"FooBar", "FOO_BAR"},
+ {"FooBarBaz", "FOO_BAR_BAZ"},
+ {"fooBar", "FOO_BAR"},
+ {"fooBarBaz", "FOO_BAR_BAZ"},
+ {"", ""},
+ {"Foo_", "FOO_"},
+ {"Foo_Bar", "FOO_BAR"},
+ {"Foo_bar", "FOO_BAR"},
+ {"Foo_Bar_Baz", "FOO_BAR_BAZ"},
+ } {
+ if got := camelToSnake(tc[0]); got != tc[1] {
+ t.Fatalf("expected camelToSnake(%q) to be %q, got %q", tc[0], tc[1], got)
+ }
+ }
+}