// Package variables provides dynamic variable expansion for rules package variables import ( "fmt" "regexp" "strings" "time" ) // Context holds data for variable expansion type Context struct { Object map[string]string Rule map[string]string Now time.Time Custom map[string]string } // NewContext creates a new variable expansion context func NewContext() *Context { return &Context{ Object: make(map[string]string), Rule: make(map[string]string), Now: time.Now().UTC(), Custom: make(map[string]string), } } // WithObject sets object attributes func (c *Context) WithObject(attrs map[string][]string) *Context { for k, v := range attrs { if len(v) > 0 { c.Object[k] = v[0] } } return c } // WithRule sets rule metadata func (c *Context) WithRule(name, id string) *Context { c.Rule["name"] = name c.Rule["id"] = id return c } // Expander handles variable expansion type Expander struct { variablePattern *regexp.Regexp } // NewExpander creates a new variable expander func NewExpander() *Expander { // Match {{source.property}} patterns return &Expander{ variablePattern: regexp.MustCompile(`\{\{([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\}\}`), } } // Expand expands all variables in a template string func (e *Expander) Expand(template string, ctx *Context) (string, error) { var errors []string result := e.variablePattern.ReplaceAllStringFunc(template, func(match string) string { // Extract variable path (remove {{ and }}) path := match[2 : len(match)-2] value, err := e.resolve(path, ctx) if err != nil { errors = append(errors, err.Error()) return match // Keep original on error } return value }) if len(errors) > 0 { return result, fmt.Errorf("variable expansion errors: %s", strings.Join(errors, "; ")) } return result, nil } // ExpandStrict expands variables and fails on any unresolved variable func (e *Expander) ExpandStrict(template string, ctx *Context) (string, error) { result, err := e.Expand(template, ctx) if err != nil { return "", err } // Check for any remaining unexpanded variables remaining := e.variablePattern.FindAllString(result, -1) if len(remaining) > 0 { return "", fmt.Errorf("unresolved variables: %s", strings.Join(remaining, ", ")) } return result, nil } func (e *Expander) resolve(path string, ctx *Context) (string, error) { parts := strings.SplitN(path, ".", 2) if len(parts) != 2 { return "", fmt.Errorf("invalid variable path: %s", path) } source := parts[0] property := parts[1] switch source { case "object": if val, ok := ctx.Object[property]; ok { return val, nil } return "", fmt.Errorf("object property not found: %s", property) case "rule": if val, ok := ctx.Rule[property]; ok { return val, nil } return "", fmt.Errorf("rule property not found: %s", property) case "now": return e.resolveNow(property, ctx.Now) case "custom": if val, ok := ctx.Custom[property]; ok { return val, nil } return "", fmt.Errorf("custom property not found: %s", property) default: return "", fmt.Errorf("unknown variable source: %s", source) } } func (e *Expander) resolveNow(property string, now time.Time) (string, error) { switch property { case "utc_date": return now.Format("2006-01-02"), nil case "utc_time": return now.Format("15:04:05"), nil case "utc_datetime": return now.Format("2006-01-02T15:04:05Z"), nil case "year": return now.Format("2006"), nil case "month": return now.Format("01"), nil case "day": return now.Format("02"), nil case "hour": return now.Format("15"), nil case "minute": return now.Format("04"), nil default: return "", fmt.Errorf("unknown now property: %s", property) } } // ListVariables extracts all variable references from a template func (e *Expander) ListVariables(template string) []string { matches := e.variablePattern.FindAllString(template, -1) unique := make(map[string]bool) var result []string for _, m := range matches { if !unique[m] { unique[m] = true result = append(result, m) } } return result } // ValidateTemplate checks if a template has valid variable syntax func (e *Expander) ValidateTemplate(template string) []string { var warnings []string // Check for unclosed braces openCount := strings.Count(template, "{{") closeCount := strings.Count(template, "}}") if openCount != closeCount { warnings = append(warnings, "mismatched variable delimiters") } return warnings }