updated ezconf
This commit is contained in:
178
ezconf/parser.go
178
ezconf/parser.go
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user