feat: message parser with json/regex/text modes
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONParser struct{}
|
||||||
|
|
||||||
|
func (p *JSONParser) Parse(body []byte) (*ParsedMessage, error) {
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &raw); err != nil {
|
||||||
|
return nil, fmt.Errorf("json parse: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
event, ok := raw["event"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("json: missing 'event' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := raw["data"].(map[string]interface{})
|
||||||
|
if data == nil {
|
||||||
|
// If no "data" key, use the entire JSON as data (minus event)
|
||||||
|
data = make(map[string]interface{})
|
||||||
|
for k, v := range raw {
|
||||||
|
if k != "event" {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ParsedMessage{
|
||||||
|
Event: event,
|
||||||
|
Data: data,
|
||||||
|
Body: string(body),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParsedMessage is the result of parsing an incoming notification body.
|
||||||
|
type ParsedMessage struct {
|
||||||
|
Event string
|
||||||
|
Data map[string]interface{} // field name → value, passed to template
|
||||||
|
Body string // raw body (for text mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser converts a raw HTTP body into a ParsedMessage.
|
||||||
|
type Parser interface {
|
||||||
|
Parse(body []byte) (*ParsedMessage, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParser(parseMode string, parsePattern string) (Parser, error) {
|
||||||
|
switch parseMode {
|
||||||
|
case "json":
|
||||||
|
return &JSONParser{}, nil
|
||||||
|
case "regex":
|
||||||
|
if parsePattern == "" {
|
||||||
|
return nil, fmt.Errorf("regex parse mode requires parse_pattern")
|
||||||
|
}
|
||||||
|
return NewRegexParser(parsePattern)
|
||||||
|
case "text":
|
||||||
|
return &TextParser{}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown parse_mode: %s", parseMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJSONParser(t *testing.T) {
|
||||||
|
p := &JSONParser{}
|
||||||
|
body := []byte(`{"event":"trade.open","data":{"symbol":"BTC","price":65000}}`)
|
||||||
|
msg, err := p.Parse(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if msg.Event != "trade.open" {
|
||||||
|
t.Errorf("expected event 'trade.open', got '%s'", msg.Event)
|
||||||
|
}
|
||||||
|
if msg.Data["symbol"] != "BTC" {
|
||||||
|
t.Errorf("expected symbol 'BTC', got '%v'", msg.Data["symbol"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONParser_NoDataKey(t *testing.T) {
|
||||||
|
p := &JSONParser{}
|
||||||
|
body := []byte(`{"event":"alert","symbol":"ETH"}`)
|
||||||
|
msg, err := p.Parse(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if msg.Data["symbol"] != "ETH" {
|
||||||
|
t.Errorf("expected symbol 'ETH' in data, got '%v'", msg.Data["symbol"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegexParser(t *testing.T) {
|
||||||
|
p, err := NewRegexParser(`(?P<event>[\w.]+) (?P<message>.+)`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
body := []byte("trade.open BTC long at 65000")
|
||||||
|
msg, err := p.Parse(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if msg.Event != "trade.open" {
|
||||||
|
t.Errorf("expected event 'trade.open', got '%s'", msg.Event)
|
||||||
|
}
|
||||||
|
if msg.Data["message"] != "BTC long at 65000" {
|
||||||
|
t.Errorf("expected message, got '%v'", msg.Data["message"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTextParser(t *testing.T) {
|
||||||
|
p := &TextParser{}
|
||||||
|
body := []byte("raw notification text here")
|
||||||
|
msg, err := p.Parse(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if msg.Data["Body"] != "raw notification text here" {
|
||||||
|
t.Errorf("expected Body in data, got '%v'", msg.Data["Body"])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegexParser struct {
|
||||||
|
re *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegexParser(pattern string) (*RegexParser, error) {
|
||||||
|
re, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("compile regex pattern: %w", err)
|
||||||
|
}
|
||||||
|
return &RegexParser{re: re}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RegexParser) Parse(body []byte) (*ParsedMessage, error) {
|
||||||
|
matches := p.re.FindStringSubmatch(string(body))
|
||||||
|
if matches == nil {
|
||||||
|
return nil, fmt.Errorf("regex: body does not match pattern")
|
||||||
|
}
|
||||||
|
|
||||||
|
names := p.re.SubexpNames()
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
var event string
|
||||||
|
|
||||||
|
for i, name := range names {
|
||||||
|
if i == 0 || name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if name == "event" {
|
||||||
|
event = matches[i]
|
||||||
|
} else {
|
||||||
|
data[name] = matches[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ParsedMessage{
|
||||||
|
Event: event,
|
||||||
|
Data: data,
|
||||||
|
Body: string(body),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
type TextParser struct{}
|
||||||
|
|
||||||
|
func (p *TextParser) Parse(body []byte) (*ParsedMessage, error) {
|
||||||
|
return &ParsedMessage{
|
||||||
|
Event: "default", // text mode has no explicit event
|
||||||
|
Data: map[string]interface{}{"Body": string(body)},
|
||||||
|
Body: string(body),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user