Skip to content

Commit

Permalink
Merge pull request #3 from g4s8/i2-envprefix
Browse files Browse the repository at this point in the history
feat: support struct field and envPrefix
  • Loading branch information
g4s8 authored Jan 9, 2024
2 parents 23b13f3 + f63e8e0 commit de77cea
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 55 deletions.
15 changes: 15 additions & 0 deletions _examples/envprefix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

//go:generate go run ../ -output envprefix.md -type Settings
type Settings struct {
// Database is the database settings
Database Database `envPrefix:"DB_"`

// Debug is the debug flag
Debug bool `env:"DEBUG"`
}

type Database struct {
// Port is the port to connect to
Port Int `env:"PORT,required"`
}
6 changes: 6 additions & 0 deletions _examples/envprefix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Environment Variables

## Settings

- `DB_PORT` (**required**) - Port is the port to connect to
- `DEBUG` - Debug is the debug flag
165 changes: 134 additions & 31 deletions inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ import (
"strings"
)

type envFieldKind int

const (
envFieldKindPlain envFieldKind = iota
envFieldKindStruct // struct reference
)

type envField struct {
name string
kind envFieldKind
doc string
opts EnvVarOptions
typeRef string
envPrefix string
}

type envStruct struct {
name string
doc string
fields []envField
}

type inspector struct {
typeName string // type name to generate documentation for, could be empty
all bool // generate documentation for all types in the file
Expand All @@ -18,7 +40,7 @@ type inspector struct {
fileSet *token.FileSet
lines []int
pendingType bool
items []*EnvScope
items []*envStruct
doc *doc.Package
err error
}
Expand All @@ -40,15 +62,22 @@ func (i *inspector) inspectFile(fileName string) ([]*EnvScope, error) {
}

func (i *inspector) inspect(node ast.Node) ([]*EnvScope, error) {
i.items = make([]*EnvScope, 0)
i.items = make([]*envStruct, 0)
ast.Walk(i, node)
return i.items, i.err
if i.err != nil {
return nil, i.err
}
scopes, err := i.buildScopes()
if err != nil {
return nil, fmt.Errorf("build scopes: %w", err)
}
return scopes, nil
}

func (i *inspector) getScope(t *ast.TypeSpec) *EnvScope {
func (i *inspector) getStruct(t *ast.TypeSpec) *envStruct {
typeName := t.Name.Name
for _, s := range i.items {
if s.typeName == typeName {
if s.name == typeName {
return s
}
}
Expand Down Expand Up @@ -96,28 +125,26 @@ func (i *inspector) Visit(n ast.Node) ast.Visitor {
i.pendingType = true
return i
case *ast.TypeSpec:
var generate bool
if i.typeName != "" && t.Name != nil && t.Name.Name == i.typeName {
generate = true
}
if i.typeName == "" && i.pendingType {
generate = true
}
if i.all {
generate = true
}
if !generate {
return i
i.typeName = t.Name.Name
}

if st, ok := t.Type.(*ast.StructType); ok {
scope := i.getScope(t)
str := i.getStruct(t)
debug("parsing struct %s", str.name)
for _, field := range st.Fields.List {
items := i.parseField(field)
for i, item := range items {
if item.kind == envFieldKindPlain {
debug("parsed field[%d] %s", i, item.name)
} else {
debug("parsed field[%d] %s (struct ref: %s, prefix: %s)", i, item.name, item.typeRef, item.envPrefix)
}
}
if len(items) == 0 {
continue
}
scope.Vars = append(scope.Vars, items...)
str.fields = append(str.fields, items...)
}
}
// reset pending type flag event if this type
Expand All @@ -127,7 +154,7 @@ func (i *inspector) Visit(n ast.Node) ast.Visitor {
return i
}

func (i *inspector) parseType(t *ast.TypeSpec) *EnvScope {
func (i *inspector) parseType(t *ast.TypeSpec) *envStruct {
typeName := t.Name.Name
docStr := strings.TrimSpace(t.Doc.Text())
if docStr == "" {
Expand All @@ -138,10 +165,9 @@ func (i *inspector) parseType(t *ast.TypeSpec) *EnvScope {
}
}
}
return &EnvScope{
Name: typeName,
Doc: docStr,
typeName: typeName,
return &envStruct{
name: typeName,
doc: docStr,
}
}

Expand All @@ -163,7 +189,7 @@ func getTagValues(tag, tagName string) []string {
return strings.Split(tagValue, ",")
}

func (i *inspector) parseField(f *ast.Field) (out []EnvDocItem) {
func (i *inspector) parseField(f *ast.Field) (out []envField) {
if f.Tag == nil && !i.useFieldNames {
return
}
Expand All @@ -172,19 +198,33 @@ func (i *inspector) parseField(f *ast.Field) (out []EnvDocItem) {
if t := f.Tag; t != nil {
tag = t.Value
}

envPrefix := getTagValues(tag, "envPrefix")
if len(envPrefix) > 0 && envPrefix[0] != "" {
var item envField
item.envPrefix = envPrefix[0]
item.kind = envFieldKindStruct
fieldType := f.Type.(*ast.Ident)
item.typeRef = fieldType.Name
out = []envField{item}
return
}

if !strings.Contains(tag, "env:") && !i.useFieldNames {
return
}

tagValues := getTagValues(tag, "env")
if len(tagValues) > 0 && tagValues[0] != "" {
var item EnvDocItem
item.Name = tagValues[0]
out = []EnvDocItem{item}
var item envField
item.name = tagValues[0]
item.kind = envFieldKindPlain
out = []envField{item}
} else if i.useFieldNames {
out = make([]EnvDocItem, len(f.Names))
out = make([]envField, len(f.Names))
for i, name := range f.Names {
out[i].Name = camelToSnake(name.Name)
out[i].name = camelToSnake(name.Name)
out[i].kind = envFieldKindPlain
}
} else {
return
Expand All @@ -194,7 +234,7 @@ func (i *inspector) parseField(f *ast.Field) (out []EnvDocItem) {
docStr = strings.TrimSpace(f.Comment.Text())
}
for i := range out {
out[i].Doc = docStr
out[i].doc = docStr
}

var opts EnvVarOptions
Expand Down Expand Up @@ -229,7 +269,70 @@ func (i *inspector) parseField(f *ast.Field) (out []EnvDocItem) {
}

for i := range out {
out[i].Opts = opts
out[i].opts = opts
}
return
}

func (i *inspector) buildScopes() ([]*EnvScope, error) {
scopes := make([]*EnvScope, 0, len(i.items))
for _, s := range i.items {
if !i.all && s.name != i.typeName {
debug("skip %q", s.name)
continue
}

debug("process %q", s.name)
scope := &EnvScope{
Name: s.name,
Doc: s.doc,
}
for _, f := range s.fields {
switch f.kind {
case envFieldKindPlain:
v := EnvDocItem{
Name: f.name,
Doc: f.doc,
Opts: f.opts,
}
debug("[p] add docItem: %s <- %s", scope.Name, v.Name)
scope.Vars = append(scope.Vars, v)
case envFieldKindStruct:
envPrefix := f.envPrefix
var base *envStruct
for _, s := range i.items {
if s.name == f.typeRef {
base = s
break
}
}
if base == nil {
return nil, fmt.Errorf("struct %q not found", f.typeRef)
}
for _, f := range base.fields {
name := fmt.Sprintf("%s%s", envPrefix, f.name)
v := EnvDocItem{
Name: name,
Doc: f.doc,
Opts: f.opts,
}
debug("[s] add docItem: %s <- %s (prefix: %s)", scope.Name, v.Name, envPrefix)
scope.Vars = append(scope.Vars, v)
}
default:
panic("unknown field kind")
}
}
scopes = append(scopes, scope)
}
return scopes, nil
}

const debugLogs = false

func debug(f string, args ...any) {
if !debugLogs {
return
}
fmt.Printf("DEBUG: "+f+"\n", args...)
}
53 changes: 31 additions & 22 deletions inspector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ func TestTagParsers(t *testing.T) {
}
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.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.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)
if e.Opts != a.opts {
t.Errorf("expected[%d] opts %#v, got %#v", i, e.Opts, a.opts)
}
}
})
Expand Down Expand Up @@ -249,8 +249,7 @@ func TestInspector(t *testing.T) {
all: true,
expectScopes: []EnvScope{
{
Name: "Foo",
typeName: "Foo",
Name: "Foo",
Vars: []EnvDocItem{
{
Name: "ONE",
Expand All @@ -263,8 +262,7 @@ func TestInspector(t *testing.T) {
},
},
{
Name: "Bar",
typeName: "Bar",
Name: "Bar",
Vars: []EnvDocItem{
{
Name: "THREE",
Expand All @@ -278,14 +276,28 @@ func TestInspector(t *testing.T) {
},
},
},
{
name: "envprefix.go",
typeName: "Settings",
expect: []EnvDocItem{
{
Name: "DB_PORT",
Doc: "Port is the port to connect to",
Opts: EnvVarOptions{Required: true},
},
{
Name: "DEBUG",
Doc: "Debug is the debug flag",
},
},
},
} {
scopes := c.expectScopes
if scopes == nil {
scopes = []EnvScope{
{
Name: c.typeName,
typeName: c.typeName,
Vars: c.expect,
Name: c.typeName,
Vars: c.expect,
},
}
}
Expand Down Expand Up @@ -326,30 +338,27 @@ func inspectorTester(name string, typeName string, all bool, lineN int, expect [
if len(scopes) != len(expect) {
t.Fatalf("inspector found %d scopes; expected %d", len(scopes), len(expect))
}
skipScopesCheck := len(expect) == 1 && expect[0].typeName == ""
skipScopesCheck := len(expect) == 1 && expect[0].Name == ""
for i, s := range scopes {
e := expect[i]
if !skipScopesCheck {
if s.Name != e.Name {
t.Fatalf("[%d]scope: expect name %q; expected %q", i, e.Name, s.Name)
}
if s.typeName != e.typeName {
t.Fatalf("[%d]scope: expect type name %q; expected %q", i, e.typeName, s.typeName)
t.Fatalf("[%d]scope: expect name %q; was %q", i, e.Name, s.Name)
}
if len(s.Vars) != len(e.Vars) {
t.Fatalf("[%d]scope: expect %d vars; expected %d", i, len(e.Vars), len(s.Vars))
t.Fatalf("[%d]scope: expect %d vars; was %d", i, len(e.Vars), len(s.Vars))
}
}
for j, v := range s.Vars {
ev := e.Vars[j]
if v.Name != ev.Name {
t.Fatalf("[%d]scope: var[%d]: expect name %q; expected %q", i, j, ev.Name, v.Name)
t.Fatalf("[%d]scope: var[%d]: expect name %q; was %q", i, j, ev.Name, v.Name)
}
if v.Doc != ev.Doc {
t.Fatalf("[%d]scope: var[%d]: expect doc %q; expected %q", i, j, ev.Doc, v.Doc)
t.Fatalf("[%d]scope: var[%d]: expect doc %q; was %q", i, j, ev.Doc, v.Doc)
}
if v.Opts != ev.Opts {
t.Fatalf("[%d]scope: var[%d]: expect opts %+v; expected %+v", i, j, ev.Opts, v.Opts)
t.Fatalf("[%d]scope: var[%d]: expect opts %+v; was %+v", i, j, ev.Opts, v.Opts)
}
}

Expand Down
Loading

0 comments on commit de77cea

Please sign in to comment.