feat: message parser with json/regex/text modes

This commit is contained in:
2026-06-27 13:11:05 +08:00
parent d030f0f0df
commit 69e32cb3ec
5 changed files with 189 additions and 0 deletions
+37
View File
@@ -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
}
+33
View File
@@ -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)
}
}
+62
View File
@@ -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"])
}
}
+46
View File
@@ -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
}
+11
View File
@@ -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
}