package engine import ( "bytes" "fmt" "reflect" "strings" "text/template" "aiaa-notification-service/internal/display" ) type Renderer struct{} func NewRenderer() *Renderer { return &Renderer{} } func (r *Renderer) Render(tmplContent string, data map[string]interface{}) (string, error) { tmpl, err := template.New("notify"). Option("missingkey=zero"). Funcs(template.FuncMap{ "line": templateLine, "case": templateCase, "replace": strings.ReplaceAll, }). Parse(tmplContent) if err != nil { return "", fmt.Errorf("parse template: %w", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, display.WrapMap(data)); err != nil { return "", fmt.Errorf("execute template: %w", err) } return buf.String(), nil } // templateLine renders "label:value\n" or empty if value is missing/zero. func templateLine(label string, v any) string { if isEmptyValue(v) { return "" } return strings.TrimRight(label, "::") + ":" + fmt.Sprint(v) + "\n" } func templateCase(value any, pairs ...any) string { got := stringify(value) n := len(pairs) def := got if n%2 == 1 { def = stringify(pairs[n-1]) pairs = pairs[:n-1] } for i := 0; i+1 < len(pairs); i += 2 { if caseKeyMatch(got, stringify(pairs[i])) { return stringify(pairs[i+1]) } } return def } func caseKeyMatch(value, key string) bool { v := strings.ToLower(strings.TrimSpace(value)) k := strings.ToLower(strings.TrimSpace(key)) k = strings.TrimPrefix(k, ".") vSeg := v if i := strings.LastIndex(v, "."); i >= 0 { vSeg = v[i+1:] } return k == v || k == strings.TrimPrefix(v, ".") || k == vSeg } func stringify(v any) string { if v == nil { return "" } return fmt.Sprint(v) } func isEmptyValue(v any) bool { switch t := v.(type) { case nil: return true case string: return strings.TrimSpace(t) == "" case bool: return !t default: rv := reflect.ValueOf(v) switch rv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return rv.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return rv.Uint() == 0 case reflect.Float32, reflect.Float64: return rv.Float() == 0 default: return false } } }