Skip to content

Commit

Permalink
feat: add field-names param
Browse files Browse the repository at this point in the history
If `-field-names` flag is set, envdoc will handle
field names as environment variables names converting
them from camel to snake case.
  • Loading branch information
g4s8 committed Dec 24, 2023
1 parent 3e90dd9 commit 23b13f3
Show file tree
Hide file tree
Showing 17 changed files with 298 additions and 62 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions _examples/complex-fields.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions _examples/complex-nostyle.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ <h2>NextConfig</h2>
<ul>
<li><code>MOUNT</code> (<strong>required</strong>) - Mount is a mount point.</li>

</ul>

<h2>FieldNames</h2>
<p>FieldNames uses field names as env names.</p>
<ul>
<li><code>QUUX</code> - Quux is a field with a tag.</li>

</ul>

</article>
Expand Down
16 changes: 16 additions & 0 deletions _examples/complex.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
7 changes: 7 additions & 0 deletions _examples/complex.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ <h2>NextConfig</h2>
<ul>
<li><code>MOUNT</code> (<strong>required</strong>) - Mount is a mount point.</li>

</ul>

<h2>FieldNames</h2>
<p>FieldNames uses field names as env names.</p>
<ul>
<li><code>QUUX</code> - Quux is a field with a tag.</li>

</ul>

</article>
Expand Down
6 changes: 6 additions & 0 deletions _examples/complex.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 6 additions & 0 deletions _examples/complex.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 6 additions & 0 deletions _examples/x_complex.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 9 additions & 1 deletion generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type generator struct {
tmpl template
prefix string
noStyles bool
fieldNames bool
}

type generatorOption func(*generator) error
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
115 changes: 66 additions & 49 deletions inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
}
}
}

Expand All @@ -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
}
Loading

0 comments on commit 23b13f3

Please sign in to comment.