Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: parse anonymous structure fields #6

Merged
merged 1 commit into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions _examples/complex-nostyle.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ <h2>ComplexConfig</h2>
<li><code>HOSTS</code> (separated by "<code>:</code>", <strong>required</strong>) - Hosts is a list of hosts.</li>
<li><code>WORDS</code> (comma-separated, from-file, default: <code>one,two,three</code>) - Words is just a list of words.</li>
<li><code>COMMENT</code> (<strong>required</strong>, default: <code>This is a comment.</code>) - Just a comment.</li>
<li>Anon is an anonymous structure.
<ul>
<li><code>ANON_USER</code> (<strong>required</strong>) - User is a user name.</li>
<li><code>ANON_PASS</code> (<strong>required</strong>) - Pass is a password.</li>
</ul>
</li>
</ul>

<h2>NextConfig</h2>
Expand Down
8 changes: 8 additions & 0 deletions _examples/complex.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ type ComplexConfig struct {
Words []string `env:"WORDS,file" envDefault:"one,two,three"`

Comment string `env:"COMMENT,required" envDefault:"This is a comment."` // Just a comment.

// Anon is an anonymous structure.
Anon struct {
// User is a user name.
User string `env:"USER,required"`
// Pass is a password.
Pass string `env:"PASS,required"`
} `envPrefix:"ANON_"`
}

type NextConfig struct { // NextConfig is a configuration structure.
Expand Down
6 changes: 6 additions & 0 deletions _examples/complex.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ <h2>ComplexConfig</h2>
<li><code>HOSTS</code> (separated by "<code>:</code>", <strong>required</strong>) - Hosts is a list of hosts.</li>
<li><code>WORDS</code> (comma-separated, from-file, default: <code>one,two,three</code>) - Words is just a list of words.</li>
<li><code>COMMENT</code> (<strong>required</strong>, default: <code>This is a comment.</code>) - Just a comment.</li>
<li>Anon is an anonymous structure.
<ul>
<li><code>ANON_USER</code> (<strong>required</strong>) - User is a user name.</li>
<li><code>ANON_PASS</code> (<strong>required</strong>) - Pass is a password.</li>
</ul>
</li>
</ul>

<h2>NextConfig</h2>
Expand Down
3 changes: 3 additions & 0 deletions _examples/complex.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ It is trying to cover all the possible cases.
- `HOSTS` (separated by `:`, **required**) - Hosts is a list of hosts.
- `WORDS` (comma-separated, from-file, default: `one,two,three`) - Words is just a list of words.
- `COMMENT` (**required**, default: `This is a comment.`) - Just a comment.
- Anon is an anonymous structure.
- `ANON_USER` (**required**) - User is a user name.
- `ANON_PASS` (**required**) - Pass is a password.

## NextConfig

Expand Down
3 changes: 3 additions & 0 deletions _examples/complex.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ It is trying to cover all the possible cases.
* `HOSTS` (separated by `:`, required) - Hosts is a list of hosts.
* `WORDS` (comma-separated, from-file, default: `one,two,three`) - Words is just a list of words.
* `COMMENT` (required, default: `This is a comment.`) - Just a comment.
* Anon is an anonymous structure.
* `ANON_USER` (required) - User is a user name.
* `ANON_PASS` (required) - Pass is a password.

## NextConfig

Expand Down
3 changes: 3 additions & 0 deletions _examples/x_complex.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ It is trying to cover all the possible cases.
- `X_HOSTS` (separated by `:`, **required**) - Hosts is a list of hosts.
- `X_WORDS` (comma-separated, from-file, default: `one,two,three`) - Words is just a list of words.
- `X_COMMENT` (**required**, default: `This is a comment.`) - Just a comment.
- `X_` - Anon is an anonymous structure.
- `X_ANON_USER` (**required**) - User is a user name.
- `X_ANON_PASS` (**required**) - Pass is a password.

## NextConfig

Expand Down
106 changes: 81 additions & 25 deletions inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,35 @@ type envStruct struct {
fields []envField
}

type anonymousStruct struct {
name string // generated name
doc *ast.CommentGroup
comments *ast.CommentGroup
}

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
useFieldNames bool // use field names if tag is not specified

fileSet *token.FileSet
lines []int
pendingType bool
items []*envStruct
doc *doc.Package
err error
fileSet *token.FileSet
lines []int
pendingType bool
items []*envStruct
anonymousStructs map[[2]token.Pos]anonymousStruct // map of anonymous structs by token position
doc *doc.Package
err error
}

func newInspector(typeName string, all bool, execLine int, useFieldNames bool) *inspector {
return &inspector{typeName: typeName, all: all, execLine: execLine, useFieldNames: useFieldNames}
return &inspector{
typeName: typeName,
all: all,
execLine: execLine,
useFieldNames: useFieldNames,
anonymousStructs: make(map[[2]token.Pos]anonymousStruct),
}
}

func (i *inspector) inspectFile(fileName string) ([]*EnvScope, error) {
Expand Down Expand Up @@ -126,35 +139,47 @@ func (i *inspector) Visit(n ast.Node) ast.Visitor {
i.pendingType = true
return i
case *ast.TypeSpec:
debug("type spec: %s (%T) (%d-%d)", t.Name.Name, t.Type, t.Pos(), t.End())
if i.typeName == "" && i.pendingType {
i.typeName = t.Name.Name
}

if st, ok := t.Type.(*ast.StructType); ok {
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
}
str.fields = append(str.fields, items...)
}
i.processStruct(t, st)
}
// reset pending type flag event if this type
// is not processable (e.g. interface type).
i.pendingType = false
case *ast.StructType:
posRange := [2]token.Pos{t.Pos(), t.End()}
as, ok := i.anonymousStructs[posRange]
if !ok {
return i
}
typeSpec := &ast.TypeSpec{
Name: &ast.Ident{Name: as.name},
Doc: as.doc,
Comment: as.comments,
}
i.processStruct(typeSpec, t)

debug("struct type: %T (%d-%d)", t, t.Pos(), t.End())
}
return i
}

func (i *inspector) processStruct(t *ast.TypeSpec, st *ast.StructType) {
str := i.getStruct(t)
debug("parsing struct %s", str.name)
for _, field := range st.Fields.List {
items := i.parseField(field)
if len(items) == 0 {
continue
}
str.fields = append(str.fields, items...)
}
}

func (i *inspector) parseType(t *ast.TypeSpec) *envStruct {
typeName := t.Name.Name
docStr := strings.TrimSpace(t.Doc.Text())
Expand Down Expand Up @@ -205,8 +230,28 @@ func (i *inspector) parseField(f *ast.Field) (out []envField) {
var item envField
item.envPrefix = envPrefix[0]
item.kind = envFieldKindStruct
fieldType := f.Type.(*ast.Ident)
item.typeRef = fieldType.Name
switch fieldType := f.Type.(type) {
case *ast.Ident:
item.typeRef = fieldType.Name
case *ast.StructType:
nameGen := fastRandString(16)
i.getStruct(&ast.TypeSpec{
Name: &ast.Ident{Name: nameGen},
Type: fieldType,
Doc: &ast.CommentGroup{List: f.Doc.List},
})
item.typeRef = nameGen
posRange := [2]token.Pos{fieldType.Pos(), fieldType.End()}
i.anonymousStructs[posRange] = anonymousStruct{
name: nameGen,
doc: f.Doc,
comments: f.Comment,
}
debug("anonymous struct found: %s (%d-%d)", nameGen, f.Type.Pos(), f.Type.End())

default:
panic(fmt.Sprintf("unsupported field type: %T", f.Type))
}
fieldNames := make([]string, len(f.Names))
for i, name := range f.Names {
fieldNames[i] = name.Name
Expand Down Expand Up @@ -288,6 +333,17 @@ func (i *inspector) buildScopes() ([]*EnvScope, error) {
debug("skip %q", s.name)
continue
}
var isAnonymous bool
for _, f := range i.anonymousStructs {
if f.name == s.name {
isAnonymous = true
break
}
}
if isAnonymous {
debug("skip anonymous struct %q", s.name)
continue
}

debug("process %q", s.name)
scope := &EnvScope{
Expand Down
62 changes: 16 additions & 46 deletions inspector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,52 +277,6 @@ func TestInspector(t *testing.T) {
},
},
{
/*
type Settings struct {
// Database is the database settings
Database Database `envPrefix:"DB_"`

// Server is the server settings
Server ServerConfig `envPrefix:"SERVER_"`

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

// Database is the database settings.
type Database struct {
// Port is the port to connect to
Port Int `env:"PORT,required"`
// Host is the host to connect to
Host string `env:"HOST,nonempty" envDefault:"localhost"`
// User is the user to connect as
User string `env:"USER"`
// Password is the password to use
Password string `env:"PASSWORD"`
// DisableTLS is the flag to disable TLS
DisableTLS bool `env:"DISABLE_TLS"`
}

// ServerConfig is the server settings.
type ServerConfig struct {
// Port is the port to listen on
Port Int `env:"PORT,required"`

// Host is the host to listen on
Host string `env:"HOST,nonempty" envDefault:"localhost"`

// Timeout is the timeout settings
Timeout TimeoutConfig `envPrefix:"TIMEOUT_"`
}

// TimeoutConfig is the timeout settings.
type TimeoutConfig struct {
// Read is the read timeout
Read Int `env:"READ" envDefault:"30"`
// Write is the write timeout
Write Int `env:"WRITE" envDefault:"30"`
}
*/
name: "envprefix.go",
typeName: "Settings",
expect: []EnvDocItem{
Expand Down Expand Up @@ -392,6 +346,22 @@ func TestInspector(t *testing.T) {
},
},
},
{
name: "anonymous.go",
typeName: "Config",
expect: []EnvDocItem{
{
Doc: "Repo is the configuration for the repository.",
Children: []EnvDocItem{
{
Name: "REPO_CONN",
Doc: "Conn is the connection string for the repository.",
Opts: EnvVarOptions{Required: true, NonEmpty: true},
},
},
},
},
},
} {
scopes := c.expectScopes
if scopes == nil {
Expand Down
10 changes: 10 additions & 0 deletions testdata/anonymous.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

// Config is the configuration for the application.
type Config struct {
// Repo is the configuration for the repository.
Repo struct {
// Conn is the connection string for the repository.
Conn string `env:"CONN,notEmpty"`
} `envPrefix:"REPO_"`
}
11 changes: 11 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"io"
"math/rand"
"strings"
"unicode"
)
Expand Down Expand Up @@ -29,3 +30,13 @@ func camelToSnake(s string) string {

return result.String()
}

func fastRandString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
seed := rand.Intn(len(letters)*len(letters)) + 1
b := make([]byte, n)
for i := range b {
b[i] = letters[(seed+i)%len(letters)]
}
return string(b)
}
Loading