Webhook-to-SSE gateway with hierarchical topic routing and signature verification
at main 84 lines 1.5 kB view raw
1package main 2 3import ( 4 "fmt" 5 "net/url" 6 "strings" 7) 8 9type Filter struct { 10 Path string 11 Value string 12} 13 14func ParseFilters(query url.Values) []Filter { 15 raw := query["filter"] 16 if len(raw) == 0 { 17 return nil 18 } 19 filters := make([]Filter, 0, len(raw)) 20 for _, f := range raw { 21 path, value, ok := strings.Cut(f, ":") 22 if !ok { 23 continue 24 } 25 filters = append(filters, Filter{Path: path, Value: value}) 26 } 27 return filters 28} 29 30func MatchAll(filters []Filter, event *Event) bool { 31 for _, f := range filters { 32 if !matchOne(f, event) { 33 return false 34 } 35 } 36 return true 37} 38 39func matchOne(f Filter, event *Event) bool { 40 val, ok := resolveField(f.Path, event) 41 if !ok { 42 return false 43 } 44 return val == f.Value 45} 46 47func resolveField(path string, event *Event) (string, bool) { 48 parts := strings.Split(path, ".") 49 50 switch parts[0] { 51 case "id": 52 return event.ID, len(parts) == 1 53 case "path": 54 return event.Path, len(parts) == 1 55 case "timestamp": 56 return event.Timestamp.Format("2006-01-02T15:04:05Z07:00"), len(parts) == 1 57 case "headers": 58 if len(parts) != 2 { 59 return "", false 60 } 61 val, ok := event.Headers[parts[1]] 62 return val, ok 63 case "payload": 64 return navigateJSON(event.Payload, parts[1:]) 65 default: 66 return "", false 67 } 68} 69 70func navigateJSON(val any, parts []string) (string, bool) { 71 if len(parts) == 0 { 72 return fmt.Sprintf("%v", val), true 73 } 74 75 m, ok := val.(map[string]any) 76 if !ok { 77 return "", false 78 } 79 next, ok := m[parts[0]] 80 if !ok { 81 return "", false 82 } 83 return navigateJSON(next, parts[1:]) 84}