Skip to content

Commit

Permalink
Merge pull request #6 from g4s8/anon-struct
Browse files Browse the repository at this point in the history
fix: parse anonymous structure fields
  • Loading branch information
g4s8 committed Feb 6, 2024
2 parents caab31a + c15aa83 commit 445e54b
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 71 deletions.
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)
}

0 comments on commit 445e54b

Please sign in to comment.