Files
golib/ezconf/parser.go

147 lines
3.7 KiB
Go

package ezconf
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
)
// ParseConfigFile parses a Go source file and extracts ENV comments from struct fields
func ParseConfigFile(filename string) ([]EnvVar, error) {
content, err := os.ReadFile(filename)
if err != nil {
return nil, errors.Wrap(err, "failed to read file")
}
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, filename, content, parser.ParseComments)
if err != nil {
return nil, errors.Wrap(err, "failed to parse file")
}
envVars := make([]EnvVar, 0)
// Walk through the AST
ast.Inspect(file, func(n ast.Node) bool {
// Look for struct type declarations
typeSpec, ok := n.(*ast.TypeSpec)
if !ok {
return true
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
return true
}
// Iterate through struct fields
for _, field := range structType.Fields.List {
var comment string
// Try to get from doc comment (comment before field)
if field.Doc != nil && len(field.Doc.List) > 0 {
comment = field.Doc.List[0].Text
comment = strings.TrimPrefix(comment, "//")
comment = strings.TrimSpace(comment)
}
// Try to get from inline comment (comment after field)
if comment == "" && field.Comment != nil && len(field.Comment.List) > 0 {
comment = field.Comment.List[0].Text
comment = strings.TrimPrefix(comment, "//")
comment = strings.TrimSpace(comment)
}
// Parse ENV comment
if strings.HasPrefix(comment, "ENV ") {
envVar, err := parseEnvComment(comment)
if err == nil {
envVars = append(envVars, *envVar)
}
}
}
return true
})
return envVars, nil
}
// ParseConfigPackage parses all Go files in a package directory and extracts ENV comments
func ParseConfigPackage(packagePath string) ([]EnvVar, error) {
// Find all .go files in the package
files, err := filepath.Glob(filepath.Join(packagePath, "*.go"))
if err != nil {
return nil, errors.Wrap(err, "failed to glob package files")
}
allEnvVars := make([]EnvVar, 0)
for _, file := range files {
// Skip test files
if strings.HasSuffix(file, "_test.go") {
continue
}
envVars, err := ParseConfigFile(file)
if err != nil {
// Log error but continue with other files
continue
}
allEnvVars = append(allEnvVars, envVars...)
}
return allEnvVars, nil
}
// parseEnvComment parses a field comment to extract environment variable information.
// Expected format: ENV ENV_NAME: Description (required <condition>) (default: <value>)
func parseEnvComment(comment string) (*EnvVar, error) {
// Check if comment starts with ENV
if !strings.HasPrefix(comment, "ENV ") {
return nil, errors.New("comment does not start with 'ENV '")
}
// Remove "ENV " prefix
comment = strings.TrimPrefix(comment, "ENV ")
// Extract env var name (everything before the first colon)
colonIdx := strings.Index(comment, ":")
if colonIdx == -1 {
return nil, errors.New("missing colon separator")
}
envVar := &EnvVar{
Name: strings.TrimSpace(comment[:colonIdx]),
}
// Extract description and optional parts
remainder := strings.TrimSpace(comment[colonIdx+1:])
// Check for (required ...) pattern
requiredPattern := regexp.MustCompile(`\(required[^)]*\)`)
if requiredPattern.MatchString(remainder) {
envVar.Required = true
remainder = requiredPattern.ReplaceAllString(remainder, "")
}
// Check for (default: ...) pattern
defaultPattern := regexp.MustCompile(`\(default:\s*([^)]*)\)`)
if matches := defaultPattern.FindStringSubmatch(remainder); len(matches) > 1 {
envVar.Default = strings.TrimSpace(matches[1])
remainder = defaultPattern.ReplaceAllString(remainder, "")
}
// What remains is the description
envVar.Description = strings.TrimSpace(remainder)
return envVar, nil
}