updated ezconf

This commit is contained in:
2026-02-25 21:52:57 +11:00
parent 05be28d7f3
commit 9179736c90
7 changed files with 380 additions and 387 deletions

View File

@@ -1,146 +1,102 @@
package ezconf
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"reflect"
"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")
// ParseConfigStruct extracts environment variable metadata from a config
// struct's ezconf struct tags using reflection.
//
// The configPtr parameter must be a pointer to a struct. Each field with an
// ezconf tag will be parsed to extract environment variable information.
//
// Tag format: `ezconf:"VAR_NAME,description:Description text,default:value,required"`
//
// Components:
// - First value: environment variable name (required)
// - description:...: Description of the variable
// - default:...: Default value
// - required: Marks the variable as required (optionally required:condition)
func ParseConfigStruct(configPtr any) ([]EnvVar, error) {
if configPtr == nil {
return nil, errors.New("config pointer cannot be nil")
}
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, filename, content, parser.ParseComments)
if err != nil {
return nil, errors.Wrap(err, "failed to parse file")
v := reflect.ValueOf(configPtr)
if v.Kind() != reflect.Ptr {
return nil, errors.New("config must be a pointer to a struct")
}
v = v.Elem()
if v.Kind() != reflect.Struct {
return nil, errors.New("config must be a pointer to a struct")
}
t := v.Type()
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
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("ezconf")
if tag == "" {
continue
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
return true
envVar, err := parseEzconfTag(tag)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse ezconf tag on field %s", field.Name)
}
// 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
})
envVars = append(envVars, *envVar)
}
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")
// parseEzconfTag parses an ezconf struct tag value to extract environment
// variable information.
//
// Expected format: "VAR_NAME,description:Description text,default:value,required"
func parseEzconfTag(tag string) (*EnvVar, error) {
if tag == "" {
return nil, errors.New("tag cannot be empty")
}
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")
parts := strings.Split(tag, ",")
if len(parts) == 0 {
return nil, errors.New("tag cannot be empty")
}
envVar := &EnvVar{
Name: strings.TrimSpace(comment[:colonIdx]),
Name: strings.TrimSpace(parts[0]),
}
// 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, "")
if envVar.Name == "" {
return nil, errors.New("environment variable name cannot be empty")
}
// 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, "")
}
for _, part := range parts[1:] {
part = strings.TrimSpace(part)
// What remains is the description
envVar.Description = strings.TrimSpace(remainder)
switch {
case strings.HasPrefix(part, "description:"):
envVar.Description = strings.TrimSpace(strings.TrimPrefix(part, "description:"))
case strings.HasPrefix(part, "default:"):
envVar.Default = strings.TrimSpace(strings.TrimPrefix(part, "default:"))
case part == "required":
envVar.Required = true
case strings.HasPrefix(part, "required:"):
envVar.Required = true
// Store the condition in the description if it adds context
condition := strings.TrimSpace(strings.TrimPrefix(part, "required:"))
if condition != "" && envVar.Description != "" {
envVar.Description = envVar.Description + " (required " + condition + ")"
}
}
}
return envVar, nil
}