diff --git a/internal/parser/json.go b/internal/parser/json.go new file mode 100644 index 0000000..e9710f9 --- /dev/null +++ b/internal/parser/json.go @@ -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 +} diff --git a/internal/parser/parser.go b/internal/parser/parser.go new file mode 100644 index 0000000..bcb7fb8 --- /dev/null +++ b/internal/parser/parser.go @@ -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) + } +} diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go new file mode 100644 index 0000000..e0310db --- /dev/null +++ b/internal/parser/parser_test.go @@ -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[\w.]+) (?P.+)`) + 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"]) + } +} diff --git a/internal/parser/regex.go b/internal/parser/regex.go new file mode 100644 index 0000000..83a4dd8 --- /dev/null +++ b/internal/parser/regex.go @@ -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 +} diff --git a/internal/parser/text.go b/internal/parser/text.go new file mode 100644 index 0000000..51d7443 --- /dev/null +++ b/internal/parser/text.go @@ -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 +}