Update dependencies, enhance NATS consumer configuration, and improve error handling in message processing. Introduce telemetry support with OpenTelemetry for tracing and metrics. Refactor README to include new configuration options and update tests for improved coverage of error scenarios.

This commit is contained in:
windyboy
2025-11-15 09:01:24 +08:00
parent 31839cf21f
commit 359e7694ab
31 changed files with 1326 additions and 667 deletions
+13
View File
@@ -0,0 +1,13 @@
package mapper
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestMapper(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Adapter Mapper Suite")
}
+31 -6
View File
@@ -43,6 +43,11 @@ func (m *TelegramMapper) ToDBRow(msg *domain.ParsedMessage) ([]interface{}, erro
// SecondaryAddresses is already a string, so we can use it directly
secondaryAddresses := msg.SecondaryAddresses
status := msg.Status
if status == "" {
status = domain.MessageStatusUnknown
}
return []interface{}{
msgUUID, // uuid
msg.MessageID, // message_id
@@ -55,6 +60,8 @@ func (m *TelegramMapper) ToDBRow(msg *domain.ParsedMessage) ([]interface{}, erro
msg.Category, // category
msg.Content, // content (TEXT, original message)
bodyDataJSON, // body_data (JSONB)
string(status), // status
msg.ErrorReason, // error_reason
msg.ReceivedAt, // received_at
msg.ParsedAt, // parsed_at
msg.DispatchedAt, // dispatched_at
@@ -64,7 +71,7 @@ func (m *TelegramMapper) ToDBRow(msg *domain.ParsedMessage) ([]interface{}, erro
// FromDBRow converts a database row to a domain.ParsedMessage
func (m *TelegramMapper) FromDBRow(row []interface{}) (*domain.ParsedMessage, error) {
const expectedColumns = 15
const expectedColumns = 17
if len(row) < expectedColumns {
return nil, fmt.Errorf("expected %d columns, got %d", expectedColumns, len(row))
}
@@ -94,6 +101,19 @@ func (m *TelegramMapper) FromDBRow(row []interface{}) (*domain.ParsedMessage, er
return time.Time{}
}
toBool := func(v interface{}) bool {
switch val := v.(type) {
case bool:
return val
case *bool:
return val != nil && *val
case int64:
return val != 0
default:
return false
}
}
var bodyData interface{}
if raw := row[10]; raw != nil {
switch val := raw.(type) {
@@ -108,7 +128,10 @@ func (m *TelegramMapper) FromDBRow(row []interface{}) (*domain.ParsedMessage, er
}
}
needDispatch, _ := row[14].(bool)
status := domain.MessageStatusUnknown
if rawStatus := toString(row[11]); rawStatus != "" {
status = domain.MessageStatus(rawStatus)
}
return &domain.ParsedMessage{
Uuid: msgUUID.String(),
@@ -122,9 +145,11 @@ func (m *TelegramMapper) FromDBRow(row []interface{}) (*domain.ParsedMessage, er
Category: toString(row[8]),
Content: toString(row[9]),
BodyData: bodyData,
ReceivedAt: parseTime(row[11]),
ParsedAt: parseTime(row[12]),
DispatchedAt: parseTime(row[13]),
NeedDispatch: needDispatch,
Status: status,
ErrorReason: toString(row[12]),
ReceivedAt: parseTime(row[13]),
ParsedAt: parseTime(row[14]),
DispatchedAt: parseTime(row[15]),
NeedDispatch: toBool(row[16]),
}, nil
}
+51 -54
View File
@@ -1,71 +1,68 @@
package mapper
import (
"testing"
"time"
"caatsm/internal/domain"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestTelegramMapper_ToDBRow_GeneratesUUIDWhenEmpty(t *testing.T) {
mapper := NewTelegramMapper()
msg := &domain.ParsedMessage{}
var _ = Describe("TelegramMapper", func() {
var mapper *TelegramMapper
row, err := mapper.ToDBRow(msg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
BeforeEach(func() {
mapper = NewTelegramMapper()
})
value, ok := row[0].(uuid.UUID)
if !ok {
t.Fatalf("expected first column to be uuid.UUID, got %T", row[0])
}
if value == uuid.Nil {
t.Fatalf("expected generated uuid to be non-nil")
}
}
Describe("ToDBRow", func() {
It("generates a UUID when missing", func() {
msg := &domain.ParsedMessage{}
func TestTelegramMapper_FromDBRow_RoundTrip(t *testing.T) {
mapper := NewTelegramMapper()
now := time.Now().UTC()
row, err := mapper.ToDBRow(msg)
Expect(err).NotTo(HaveOccurred())
original := &domain.ParsedMessage{
Uuid: uuid.NewString(),
MessageID: "TMQ1324",
DateTime: "150631",
PriorityIndicator: "FF",
PrimaryAddress: "ZBTJZPZX",
SecondaryAddresses: "150630 ZBACZQZX",
Originator: "ORIGIN",
OriginatorDateTime: "150630",
Category: "FPL",
Content: "raw telegram",
BodyData: map[string]string{"key": "value"},
ReceivedAt: now,
ParsedAt: now,
DispatchedAt: now,
NeedDispatch: true,
}
value, ok := row[0].(uuid.UUID)
Expect(ok).To(BeTrue())
Expect(value).NotTo(Equal(uuid.Nil))
})
})
row, err := mapper.ToDBRow(original)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Describe("FromDBRow", func() {
It("round-trips telegram data", func() {
now := time.Now().UTC()
original := &domain.ParsedMessage{
Uuid: uuid.NewString(),
MessageID: "TMQ1324",
DateTime: "150631",
PriorityIndicator: "FF",
PrimaryAddress: "ZBTJZPZX",
SecondaryAddresses: "150630 ZBACZQZX",
Originator: "ORIGIN",
OriginatorDateTime: "150630",
Category: "FPL",
Content: "raw telegram",
BodyData: map[string]string{"key": "value"},
ReceivedAt: now,
ParsedAt: now,
DispatchedAt: now,
NeedDispatch: true,
Status: domain.MessageStatusParsed,
}
roundTrip, err := mapper.FromDBRow(row)
if err != nil {
t.Fatalf("unexpected error reading row: %v", err)
}
row, err := mapper.ToDBRow(original)
Expect(err).NotTo(HaveOccurred())
Expect(row).To(HaveLen(17))
if roundTrip.Uuid != original.Uuid {
t.Fatalf("expected uuid %s, got %s", original.Uuid, roundTrip.Uuid)
}
if roundTrip.MessageID != original.MessageID {
t.Fatalf("expected message_id %s, got %s", original.MessageID, roundTrip.MessageID)
}
if roundTrip.NeedDispatch != original.NeedDispatch {
t.Fatalf("expected need_dispatch %v, got %v", original.NeedDispatch, roundTrip.NeedDispatch)
}
}
roundTrip, err := mapper.FromDBRow(row)
Expect(err).NotTo(HaveOccurred())
Expect(roundTrip.Uuid).To(Equal(original.Uuid))
Expect(roundTrip.MessageID).To(Equal(original.MessageID))
Expect(roundTrip.NeedDispatch).To(Equal(original.NeedDispatch))
Expect(roundTrip.Status).To(Equal(original.Status))
})
})
})
+1 -2
View File
@@ -14,8 +14,7 @@ func NewAviationParser() *AviationParser {
}
// Parse parses a raw message string and returns a ParsedMessage
func (p *AviationParser) Parse(rawText string) *domain.ParsedMessage {
func (p *AviationParser) Parse(rawText string) (*domain.ParsedMessage, error) {
// Use the existing Parse function from internal/parsers
return parsers.Parse(rawText)
}
+1 -3
View File
@@ -5,7 +5,5 @@ import "caatsm/internal/domain"
// Parser defines the interface for parsing raw telegram messages
type Parser interface {
// Parse parses a raw message string and returns a ParsedMessage
Parse(rawText string) *domain.ParsedMessage
// TODO: consider returning (*domain.ParsedMessage, error) to surface parse failures explicitly.
Parse(rawText string) (*domain.ParsedMessage, error)
}
+3
View File
@@ -12,5 +12,8 @@ type Repository interface {
// InsertBatch inserts multiple telegram messages in a batch
InsertBatch(ctx context.Context, msgs []*domain.ParsedMessage) error
// InsertRaw captures an unparsed or failed telegram for later analysis.
InsertRaw(ctx context.Context, msg *domain.ParsedMessage) error
}