Add configuration for Code Review Automation and enhance .gitignore. Introduce .coderabbit.yml for automated reviews with profiles for correctness, maintainability, security, and performance. Update paths to include relevant directories and exclude generated files. Modify .gitignore to include coverage reports and generated files. Refactor Docker Compose to use updated paths for database initialization scripts. Update Go module dependencies and enhance Makefile with new code generation tasks. Transition domain models to a new DTO structure for better separation of concerns.

This commit is contained in:
windyboy
2025-11-17 11:47:23 +08:00
parent 12fda00df9
commit 204217015d
45 changed files with 252 additions and 175 deletions
+52
View File
@@ -0,0 +1,52 @@
package dto
import (
"time"
)
// MessageStatus represents the parsing status of a telegram.
// It is intentionally decoupled from infrastructure concerns (e.g. DB or publish failures)
// so that domain parsing state can be reasoned about independently.
type MessageStatus string
const (
MessageStatusUnknown MessageStatus = "unknown"
MessageStatusParsed MessageStatus = "parsed"
MessageStatusHeaderError MessageStatus = "header_error"
MessageStatusBodyError MessageStatus = "body_error"
)
// ParsedTelegram holds the parsed data from an aviation message.
// It is a transport-oriented model used by the application pipeline (parser,
// persistence, publishing), and may embed domain-specific body structures
// (e.g. *domain.FPL, *domain.DEP) in BodyData.
type ParsedTelegram struct {
Uuid string `json:"uuid"`
MessageID string `json:"messageId"`
DateTime string `json:"dateTime"`
PriorityIndicator string `json:"priorityIndicator"`
PrimaryAddress string `json:"primaryAddress"`
SecondaryAddresses string `json:"secondaryAddresses,omitempty"`
Originator string `json:"originator,omitempty"`
OriginatorDateTime string `json:"originatorDateTime,omitempty"`
Category string `json:"category,omitempty"`
Body string `json:"body"`
Content string `json:"content,omitempty"`
BodyData interface{} `json:"bodyData,omitempty"`
ReceivedAt time.Time `json:"receivedAt"`
ParsedAt time.Time `json:"parsedAt,omitempty"`
DispatchedAt time.Time `json:"dispatchedAt,omitempty"`
NeedDispatch bool `json:"needDispatch"`
Parsed bool `json:"parsed"`
Comments string `json:"comments,omitempty"`
Status MessageStatus
ErrorReason string `json:"errorReason,omitempty"`
}
// NewParsedTelegram initializes a ParsedTelegram with default values.
func NewParsedTelegram() *ParsedTelegram {
return &ParsedTelegram{
Parsed: false,
Status: MessageStatusUnknown,
}
}
+3 -3
View File
@@ -1,13 +1,13 @@
package mapper
import "caatsm/internal/model"
import "caatsm/internal/adapter/dto"
// Mapper defines the interface for mapping between pipeline models and database models
type Mapper interface {
// ToDBRow converts a ParsedTelegram to a database row representation
ToDBRow(msg *model.ParsedTelegram) ([]interface{}, error)
ToDBRow(msg *dto.ParsedTelegram) ([]interface{}, error)
// FromDBRow converts a database row to a ParsedTelegram
FromDBRow(row []interface{}) (*model.ParsedTelegram, error)
FromDBRow(row []interface{}) (*dto.ParsedTelegram, error)
}
+7 -7
View File
@@ -1,7 +1,7 @@
package mapper
import (
"caatsm/internal/model"
"caatsm/internal/adapter/dto"
"encoding/json"
"fmt"
"time"
@@ -18,7 +18,7 @@ func NewTelegramMapper() *TelegramMapper {
}
// ToDBRow converts a ParsedTelegram to a database row representation
func (m *TelegramMapper) ToDBRow(msg *model.ParsedTelegram) ([]interface{}, error) {
func (m *TelegramMapper) ToDBRow(msg *dto.ParsedTelegram) ([]interface{}, error) {
// Parse UUID
var msgUUID uuid.UUID
var err error
@@ -45,7 +45,7 @@ func (m *TelegramMapper) ToDBRow(msg *model.ParsedTelegram) ([]interface{}, erro
status := msg.Status
if status == "" {
status = model.MessageStatusUnknown
status = dto.MessageStatusUnknown
}
return []interface{}{
@@ -70,7 +70,7 @@ func (m *TelegramMapper) ToDBRow(msg *model.ParsedTelegram) ([]interface{}, erro
}
// FromDBRow converts a database row to a ParsedTelegram
func (m *TelegramMapper) FromDBRow(row []interface{}) (*model.ParsedTelegram, error) {
func (m *TelegramMapper) FromDBRow(row []interface{}) (*dto.ParsedTelegram, error) {
const expectedColumns = 17
if len(row) < expectedColumns {
return nil, fmt.Errorf("expected %d columns, got %d", expectedColumns, len(row))
@@ -128,12 +128,12 @@ func (m *TelegramMapper) FromDBRow(row []interface{}) (*model.ParsedTelegram, er
}
}
status := model.MessageStatusUnknown
status := dto.MessageStatusUnknown
if rawStatus := toString(row[11]); rawStatus != "" {
status = model.MessageStatus(rawStatus)
status = dto.MessageStatus(rawStatus)
}
return &model.ParsedTelegram{
return &dto.ParsedTelegram{
Uuid: msgUUID.String(),
MessageID: toString(row[1]),
DateTime: toString(row[2]),
+4 -4
View File
@@ -3,7 +3,7 @@ package mapper
import (
"time"
"caatsm/internal/model"
"caatsm/internal/adapter/dto"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
@@ -19,7 +19,7 @@ var _ = Describe("TelegramMapper", func() {
Describe("ToDBRow", func() {
It("generates a UUID when missing", func() {
msg := &model.ParsedTelegram{}
msg := &dto.ParsedTelegram{}
row, err := mapper.ToDBRow(msg)
Expect(err).NotTo(HaveOccurred())
@@ -33,7 +33,7 @@ var _ = Describe("TelegramMapper", func() {
Describe("FromDBRow", func() {
It("round-trips telegram data", func() {
now := time.Now().UTC()
original := &model.ParsedTelegram{
original := &dto.ParsedTelegram{
Uuid: uuid.NewString(),
MessageID: "TMQ1324",
DateTime: "150631",
@@ -49,7 +49,7 @@ var _ = Describe("TelegramMapper", func() {
ParsedAt: now,
DispatchedAt: now,
NeedDispatch: true,
Status: model.MessageStatusParsed,
Status: dto.MessageStatusParsed,
}
row, err := mapper.ToDBRow(original)
+408
View File
@@ -0,0 +1,408 @@
package parser
import (
"caatsm/internal/domain"
"caatsm/internal/adapter/dto"
"errors"
"fmt"
"regexp"
"strings"
"sync"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
)
const (
SSR = "ssr"
DepartureCode = "dep"
DepartureTime = "dep_time"
ArrivalCode = "arr"
ArrivalTime = "arr_time"
DestinationCode = "dest"
OtherInfo = "other"
ReferenceData = "reference_data"
Aircraft = "aircraft"
CategorySurveillance = "surve"
Indicator = "indicator"
Other = "other"
AircraftID = "aircraft"
Surveillance = "surve"
Speed = "speed"
Level = "level"
Route = "route"
EstimatedTime = "estt"
AlternateAirport = "alter"
PBN = "pbn"
NavigationEquipment = "nav"
EstimatedElapsedTime = "eet"
SELCALCode = "sel"
PerformanceCategory = "per"
RerouteInformation = "rif"
Remarks = "remark"
)
var (
otherPatterns = []*regexp.Regexp{navPattern,
remarkPattern,
selPattern,
pbnPattern,
eetPattern,
performancePattern,
regPattern,
reroutePattern}
// ErrHeaderParse indicates an invalid header section.
ErrHeaderParse = errors.New("invalid telegram header")
// ErrBodyParse indicates a failure matching the telegram body.
ErrBodyParse = errors.New("invalid telegram body")
)
type BodyParser struct {
body string
bodyPatterns map[string]BodyConfig
mu sync.Mutex
}
func NewBodyParser(body string) *BodyParser {
return &BodyParser{
bodyPatterns: bodyPatterns,
body: body,
}
}
func (parser *BodyParser) GetBodyPatterns() map[string]BodyConfig {
parser.mu.Lock()
defer parser.mu.Unlock()
return parser.bodyPatterns
}
func (parser *BodyParser) SetBodyPatterns(patterns map[string]BodyConfig) {
parser.mu.Lock()
defer parser.mu.Unlock()
parser.bodyPatterns = patterns
}
func (parser *BodyParser) Parse() (string, interface{}, error) {
parser.mu.Lock()
defer parser.mu.Unlock()
parser.body = strings.TrimSpace(parser.body)
category := findCategory(parser.body)
if category == "" {
return "", nil, fmt.Errorf("no category found in body text")
}
if patternConfig, exists := parser.bodyPatterns[category]; exists && patternConfig.Patterns != nil {
for _, p := range patternConfig.Patterns {
if data := extract(parser.body, p.Expression); data != nil {
return parser.createBodyData(data)
}
}
}
return "", nil, fmt.Errorf("no matching pattern found for body: %s", parser.body)
}
func findCategory(body string) string {
if match := categoryRegex.FindStringSubmatch(body); match != nil {
for i, name := range categoryRegex.SubexpNames() {
if i != 0 && name == "category" {
return match[i]
}
}
}
return ""
}
func extract(data string, exp *regexp.Regexp) map[string]string {
match := exp.FindStringSubmatch(data)
if len(match) > 0 {
return extractData(match, exp)
}
return nil
}
func extractData(match []string, re *regexp.Regexp) map[string]string {
data := make(map[string]string)
for i, name := range re.SubexpNames() {
if i != 0 && name != "" {
data[name] = strings.TrimSpace(match[i])
}
}
return data
}
func (parser *BodyParser) createBodyData(data map[string]string) (string, interface{}, error) {
switch category := data["category"]; category {
case CategoryArrival:
return category, &domain.ARR{
Category: data[Category],
AircraftID: data[FlightNumber],
SSRModeAndCode: data[SSR],
DepartureAirport: data[DepartureCode],
ArrivalAirport: data[ArrivalCode],
ArrivalTime: data[ArrivalTime],
}, nil
case CategoryDeparture:
return category, &domain.DEP{
Category: data[Category],
AircraftID: data[FlightNumber],
SSRModeAndCode: data[SSR],
DepartureAirport: data[DepartureCode],
DepartureTime: data[DepartureTime],
Destination: data[ArrivalCode],
}, nil
case CategoryCancellation:
return category, &domain.CNL{
Category: data[category],
AircraftID: data[FlightNumber],
DepartureAirport: data[DepartureCode],
DestinationAirport: data[ArrivalCode],
}, nil
case CategoryDelay:
return category, &domain.DLA{
Category: data[Category],
AircraftID: data[FlightNumber],
DepartureAirport: data[DepartureCode],
NewDepartureTime: data[DepartureTime],
ArrivalAirport: data[ArrivalCode],
ArrivalTime: data[ArrivalTime],
}, nil
case CategoryFlightPlan:
otherData := parseOther(data[OtherInfo])
return category, &domain.FPL{
Category: data[Category],
FlightNumber: data[FlightNumber],
ReferenceData: data[ReferenceData],
AircraftID: data[AircraftID],
SSRModeAndCode: data[Surveillance],
FlightRulesAndType: data[Indicator],
CruisingSpeedAndLevel: data[Speed] + data[Level],
DepartureAirport: data[DepartureCode],
DepartureTime: data[DepartureTime],
Route: data[Route],
DestinationAndTotalTime: data[DestinationCode] + data[EstimatedTime],
AlternateAirport: data[AlternateAirport],
OtherInfo: data[OtherInfo],
Register: otherData[Register],
EstimatedArrivalTime: data[EstimatedTime],
PBN: otherData[PBN],
NavigationEquipment: otherData[NavigationEquipment],
EstimatedElapsedTime: otherData[EstimatedElapsedTime],
SELCALCode: otherData[SELCALCode],
PerformanceCategory: otherData[PerformanceCategory],
RerouteInformation: otherData[RerouteInformation],
Remarks: otherData[Remarks],
}, nil
default:
return category, nil, fmt.Errorf("invalid message type: %s", category)
}
}
func Parse(rawText string) (*dto.ParsedTelegram, error) {
header, err := ParseHeader(rawText)
if err != nil {
msg := dto.NewParsedTelegram()
msg.Content = rawText
msg.Comments = err.Error()
msg.ErrorReason = err.Error()
msg.Status = dto.MessageStatusHeaderError
return msg, fmt.Errorf("%w: %w", ErrHeaderParse, err)
}
bodyParser := NewBodyParser(header.Body)
category, bodyData, bodyErr := bodyParser.Parse()
header.Category = category
header.ParsedAt = time.Now()
if bodyErr != nil {
return &dto.ParsedTelegram{
MessageID: header.MessageID,
DateTime: header.DateTime,
PriorityIndicator: header.PriorityIndicator,
PrimaryAddress: header.PrimaryAddress,
SecondaryAddresses: header.SecondaryAddresses,
Originator: header.Originator,
OriginatorDateTime: header.OriginatorDateTime,
Category: header.Category,
Body: header.Body,
Content: header.Content,
ReceivedAt: header.ReceivedAt,
ParsedAt: header.ParsedAt,
Parsed: false,
Comments: bodyErr.Error(),
Status: dto.MessageStatusBodyError,
ErrorReason: bodyErr.Error(),
}, fmt.Errorf("%w: %w", ErrBodyParse, bodyErr)
}
parsed := &dto.ParsedTelegram{
MessageID: header.MessageID,
DateTime: header.DateTime,
PriorityIndicator: header.PriorityIndicator,
PrimaryAddress: header.PrimaryAddress,
SecondaryAddresses: header.SecondaryAddresses,
Originator: header.Originator,
OriginatorDateTime: header.OriginatorDateTime,
Category: header.Category,
Body: header.Body,
Content: header.Content,
BodyData: bodyData,
ReceivedAt: header.ReceivedAt,
ParsedAt: header.ParsedAt,
Parsed: true,
Status: dto.MessageStatusParsed,
ErrorReason: "",
}
parsed.Uuid = uuid.New().String()
return parsed, nil
}
func cleanMessage(text string) string {
cleanedText := emptyLineRemove.ReplaceAllString(text, "")
cleanText := strings.ReplaceAll(cleanedText, "\n\n", "\n")
if match := bodyOnly.FindStringSubmatch(cleanText); len(match) > 1 {
bodyContent := match[2]
if bodyContent[len(bodyContent)-1] == '\n' {
return bodyContent[:len(bodyContent)-1]
}
return bodyContent
}
return ""
}
// ParseHeader parses only the header portion of the message and returns a lightweight struct
// with header fields and body content. It is used internally by the aviation parser.
type Header struct {
MessageID string
DateTime string
PriorityIndicator string
PrimaryAddress string
SecondaryAddresses string
Originator string
OriginatorDateTime string
Category string
Content string
Body string
ReceivedAt time.Time
ParsedAt time.Time
}
func ParseHeader(fullMessage string) (Header, error) {
log := zap.S()
cleaned := cleanMessage(fullMessage)
lines := strings.Split(cleaned, "\n")
if len(lines) < 3 {
log.Warnf("invalid message format: %s", fullMessage)
return Header{Content: fullMessage}, fmt.Errorf("invalid message format: %s", fullMessage)
}
_, messageID, dateTime, err := parseStartIndicator(lines[0])
if err != nil {
return Header{Content: fullMessage}, err
}
priorityIndicator, primaryAddress := parsePriorityAndPrimary(lines[1])
secondaryAddresses, originator, originatorDateTime, body := parseRemainingLines(lines[2:])
return Header{
MessageID: messageID,
DateTime: dateTime,
PriorityIndicator: priorityIndicator,
PrimaryAddress: primaryAddress,
SecondaryAddresses: secondaryAddresses,
Originator: originator,
OriginatorDateTime: originatorDateTime,
Content: fullMessage,
Body: body,
ReceivedAt: time.Now(),
}, nil
}
func parseStartIndicator(line string) (string, string, string, error) {
parts := strings.Fields(line)
if len(parts) >= 3 && strings.HasPrefix(parts[0], StartIndicatorPrefix) {
return parts[0], parts[1], parts[2], nil
}
zap.S().Warnf("invalid start indicator line format: %s", line)
return "", "", "", fmt.Errorf("invalid start indicator line format: %s", line)
}
func parsePriorityAndPrimary(line string) (string, string) {
parts := strings.Fields(line)
if len(parts) >= 2 {
return parts[0], parts[1]
}
zap.S().Warnf("invalid priority and primary address line format: %s", line)
return "", ""
}
func parseRemainingLines(lines []string) (string, string, string, string) {
var (
secondaryAddresses string
originator string
originatorDateTime string
bodyAndFooter strings.Builder
headerEnded bool
)
for _, line := range lines {
line = strings.TrimSpace(line)
if headerEnded {
bodyAndFooter.WriteString(line + "\n")
} else {
switch {
case line == EndHeaderMarker:
case strings.HasPrefix(line, "."):
originatorInfo := strings.Fields(line[1:])
if len(originatorInfo) >= 2 {
originator = originatorInfo[0]
originatorDateTime = originatorInfo[1]
}
headerEnded = true
case strings.HasPrefix(line, BeginPartMarker) || strings.HasPrefix(line, "("):
headerEnded = true
if strings.Index(line, "NNNN") > 0 {
break
}
bodyAndFooter.WriteString(line + "\n")
default:
if o1, o2 := getOriginator(line); o1 != "" {
originatorDateTime = o1
originator = o2
} else {
secondaryAddresses = secondaryAddresses + " " + line
}
}
}
}
return secondaryAddresses, originator, originatorDateTime, bodyAndFooter.String()
}
func getOriginator(line string) (string, string) {
match := originator.FindStringSubmatch(line)
if len(match) >= 3 {
return match[1], match[2]
}
zap.S().Warnf("invalid originator line format: %s", line)
return "", ""
}
func parseOther(text string) map[string]string {
data := make(map[string]string)
for _, re := range otherPatterns {
if match := re.FindStringSubmatch(text); len(match) > 0 {
for i, name := range re.SubexpNames() {
if i != 0 && name != "" {
data[name] = strings.TrimSpace(match[i])
}
}
}
}
return data
}
@@ -1,20 +0,0 @@
package parser
import (
"caatsm/internal/model"
"caatsm/internal/parsers"
)
// AviationParser implements the Parser interface using the existing parsers package
type AviationParser struct{}
// NewAviationParser creates a new aviation parser
func NewAviationParser() *AviationParser {
return &AviationParser{}
}
// Parse parses a raw message string and returns a ParsedTelegram
func (p *AviationParser) Parse(rawText string) (*model.ParsedTelegram, error) {
// Use the existing Parse function from internal/parsers
return parsers.Parse(rawText)
}
@@ -0,0 +1,420 @@
package parser
import (
"caatsm/internal/domain"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Aviation Parser", func() {
Describe("ParseHeader", func() {
Context("with a real ARR context", func() {
message := `ZCZC TMQ2530 141614
GG ZBTJZXZX
141614 ZSHCZTZX
(ARR-CES5470-ZBTJ-ZSHC1614)
NNNN`
It("should get a clean body text", func() {
body := cleanMessage(message)
expected := `ZCZC TMQ2530 141614
GG ZBTJZXZX
141614 ZSHCZTZX
(ARR-CES5470-ZBTJ-ZSHC1614)`
Expect(body).To(Equal(expected))
})
})
It("should parse the header correctly", func() {
message := `
ZCZC TAF6789 160530
QU TSNZPCA
.
QU PEKUDCA TSNUOCA TSNZPCA TSNUFCA
.TAF WSSS 160500Z 1606/1712 20010KT 9999 SCT018
BECMG 1608/1610 24012KT 9999 SCT018
TEMPO 1610/1612 4000 SHRA BKN012
BECMG 1612/1614 18008KT 9999 SCT020
BEGIN PART 02
(FORECAST AMENDMENT
VALID 1606/1700
THUNDERSTORMS EXPECTED
ALTERNATE ROUTES ADVISED)
NNNN`
parsedHeader, err := ParseHeader(message)
Expect(err).ToNot(HaveOccurred())
Expect(parsedHeader.MessageID).To(Equal("TAF6789"))
Expect(parsedHeader.DateTime).To(Equal("160530"))
Expect(parsedHeader.PriorityIndicator).To(Equal("QU"))
Expect(parsedHeader.PrimaryAddress).To(Equal("TSNZPCA"))
Expect(parsedHeader.SecondaryAddresses).To(Equal(" QU PEKUDCA TSNUOCA TSNZPCA TSNUFCA"))
})
It("should parse the header correctly with originator information", func() {
message := `
ZCZC NOTAM1122 171000
QU TSNZPCA
.
QU PEKUDCA TSNUOCA TSNZPCA TSNUFCA
.SELOZKE 170999
BEGIN PART 01
RUNWAY MAINTENANCE NOTICE.
- MAINTENANCE MANAGER: JOHN DOE
RUNWAY 09/27 WILL BE CLOSED FOR MAINTENANCE FROM 0800Z TO 1600Z.
- AIRPORT OPERATIONS: SIGN . . . . . . . . . .
WE ACKNOWLEDGE THE RUNWAY CLOSURE.
- CONTROL TOWER:
SIGN . . . . . . . . . .
BEGIN PART 02
(ALERT MESSAGE - WEATHER WARNING
VALID 1500Z - 1800Z
SEVERE THUNDERSTORM FORECASTED
ALL DEPARTURES/ARRIVALS EXPECTED TO BE DELAYED)
NNNN`
parsedHeader, err := ParseHeader(message)
Expect(err).ToNot(HaveOccurred())
Expect(parsedHeader.MessageID).To(Equal("NOTAM1122"))
Expect(parsedHeader.DateTime).To(Equal("171000"))
Expect(parsedHeader.PriorityIndicator).To(Equal("QU"))
Expect(parsedHeader.PrimaryAddress).To(Equal("TSNZPCA"))
Expect(parsedHeader.SecondaryAddresses).To(Equal(" QU PEKUDCA TSNUOCA TSNZPCA TSNUFCA"))
Expect(parsedHeader.Originator).To(Equal("SELOZKE"))
Expect(parsedHeader.OriginatorDateTime).To(Equal("170999"))
})
})
Describe("Other Info", func() {
Context("PBN/A1B2B3B4B5D1L1 NAV/ABAS REG/B6513 EET/ZBPE0112 SEL/KMAL PER/C RIF/FRT N640 ZBYN RMK/TCAS EQUIPPED", func() {
It("should parse the other info correctly", func() {
otherInfo := "PBN/A1B2B3B4B5D1L1 NAV/ABAS REG/B6513 EET/ZBPE0112 SEL/KMAL PER/C RIF/FRT N640 ZBYN RMK/TCAS EQUIPPED"
parsed := parseOther(otherInfo)
Expect(parsed).ToNot(BeNil())
Expect(parsed[PBN]).To(Equal("A1B2B3B4B5D1L1"))
Expect(parsed[NavigationEquipment]).To(Equal("ABAS"))
Expect(parsed[Register]).To(Equal("B6513"))
Expect(parsed[EstimatedElapsedTime]).To(Equal("ZBPE0112"))
Expect(parsed[SELCALCode]).To(Equal("KMAL"))
Expect(parsed[PerformanceCategory]).To(Equal("C"))
Expect(parsed[RerouteInformation]).To(Equal("FRT N640 ZBYN"))
Expect(parsed[Remarks]).To(Equal("TCAS EQUIPPED"))
})
})
})
Describe("ParseBody", func() {
Context("with ARR body (ARR-CES5470-ZBTJ-ZSHC1614)", func() {
body := "(ARR-CES5470-ZBTJ-ZSHC1614)"
parser := NewBodyParser(body)
It("should parse the body correctly", func() {
category, parsedBody, err := parser.Parse()
Expect(err).ToNot(HaveOccurred())
Expect(parsedBody).ToNot(BeNil())
Expect(category).To(Equal("ARR"))
Expect(parsedBody).To(BeAssignableToTypeOf(&domain.ARR{}))
arrMessage := parsedBody.(*domain.ARR)
Expect(arrMessage.Category).To(Equal("ARR"))
Expect(arrMessage.AircraftID).To(Equal("CES5470"))
Expect(arrMessage.DepartureAirport).To(Equal("ZBTJ"))
Expect(arrMessage.ArrivalAirport).To(Equal("ZSHC"))
Expect(arrMessage.ArrivalTime).To(Equal("1614"))
})
})
Context("with ARR body", func() {
// parser := NewBodyParser(body)
It("should parse the body (ARR-AB123/A1234-KJFK-KLAX1234) correctly", func() {
body := " (ARR-AB123/A1234-KJFK-KLAX1234)"
parser := NewBodyParser(body)
category, parsedBody, err := parser.Parse()
Expect(err).ToNot(HaveOccurred())
Expect(parsedBody).ToNot(BeNil())
Expect(category).To(Equal("ARR"))
Expect(parsedBody).To(BeAssignableToTypeOf(&domain.ARR{}))
arrMessage := parsedBody.(*domain.ARR)
Expect(arrMessage.Category).To(Equal("ARR"))
Expect(arrMessage.AircraftID).To(Equal("AB123"))
Expect(arrMessage.SSRModeAndCode).To(Equal("A1234"))
Expect(arrMessage.DepartureAirport).To(Equal("KJFK"))
Expect(arrMessage.ArrivalAirport).To(Equal("KLAX"))
})
It("should parse the body (ARR-JAE7433/A0132-RKSI-ZBTJ1604) correctly", func() {
body := " (ARR-JAE7433/A0132-RKSI-ZBTJ1604)"
parser := NewBodyParser(body)
category, parsedBody, err := parser.Parse()
Expect(err).ToNot(HaveOccurred())
Expect(parsedBody).ToNot(BeNil())
Expect(category).To(Equal("ARR"))
Expect(parsedBody).To(BeAssignableToTypeOf(&domain.ARR{}))
arrMessage := parsedBody.(*domain.ARR)
Expect(arrMessage.Category).To(Equal("ARR"))
Expect(arrMessage.AircraftID).To(Equal("JAE7433"))
Expect(arrMessage.SSRModeAndCode).To(Equal("A0132"))
Expect(arrMessage.DepartureAirport).To(Equal("RKSI"))
Expect(arrMessage.ArrivalAirport).To(Equal("ZBTJ"))
})
})
Context("with DEP body", func() {
// parser := NewBodyParser()
It("should parse the body (DEP-CYZ9017/A5633-ZBTJ1638-ZSPD) correctly", func() {
body := "(DEP-CYZ9017/A5633-ZBTJ1638-ZSPD)"
parser := NewBodyParser(body)
category, parsedBody, err := parser.Parse()
Expect(err).ToNot(HaveOccurred())
Expect(parsedBody).ToNot(BeNil())
Expect(category).To(Equal("DEP"))
Expect(parsedBody).To(BeAssignableToTypeOf(&domain.DEP{}))
depMessage := parsedBody.(*domain.DEP)
Expect(depMessage.Category).To(Equal("DEP"))
Expect(depMessage.AircraftID).To(Equal("CYZ9017"))
Expect(depMessage.SSRModeAndCode).To(Equal("A5633"))
Expect(depMessage.DepartureAirport).To(Equal("ZBTJ"))
Expect(depMessage.DepartureTime).To(Equal("1638"))
Expect(depMessage.Destination).To(Equal("ZSPD"))
})
})
Context("with FPL body", func() {
// parser := NewBodyParser()
It("should parse the body correctly", func() {
body := `(FPL-CCA1532-IS
-A332/H
-SDE3FGHIJ4J5M1RWY/LB101
-ZSSS2035
-K0859S1040 PIAKS G330 PIMOL A539 BTO W82 DOGAR
-ZBAA0153 ZBYN
-PBN/A1B2B3B4B5D1L1 NAV/ABAS REG/B6513 EET/ZBPE0112 SEL/KMAL PER/C RIF/FRT N640 ZBYN RMK/TCAS EQUIPPED)`
parser := NewBodyParser(body)
category, parsedBody, err := parser.Parse()
Expect(err).ToNot(HaveOccurred())
Expect(parsedBody).ToNot(BeNil())
Expect(category).To(Equal("FPL"))
Expect(parsedBody).To(BeAssignableToTypeOf(&domain.FPL{}))
fplMessage := parsedBody.(*domain.FPL)
Expect(fplMessage.FlightNumber).To(Equal("CCA1532"))
Expect(fplMessage.FlightRulesAndType).To(Equal("IS"))
Expect(fplMessage.AircraftID).To(Equal("A332/H"))
Expect(fplMessage.SSRModeAndCode).To(Equal("SDE3FGHIJ4J5M1RWY/LB101"))
Expect(fplMessage.DepartureAirport).To(Equal("ZSSS"))
Expect(fplMessage.DepartureTime).To(Equal("2035"))
Expect(fplMessage.CruisingSpeedAndLevel).To(Equal("K0859S1040"))
Expect(fplMessage.Route).To(Equal("PIAKS G330 PIMOL A539 BTO W82 DOGAR"))
Expect(fplMessage.DestinationAndTotalTime).To(Equal("ZBAA0153"))
Expect(fplMessage.AlternateAirport).To(Equal("ZBYN"))
Expect(fplMessage.OtherInfo).To(Equal("PBN/A1B2B3B4B5D1L1 NAV/ABAS REG/B6513 EET/ZBPE0112 SEL/KMAL PER/C RIF/FRT N640 ZBYN RMK/TCAS EQUIPPED"))
Expect(fplMessage.PBN).To(Equal("A1B2B3B4B5D1L1"))
Expect(fplMessage.EstimatedElapsedTime).To(Equal("ZBPE0112"))
Expect(fplMessage.SELCALCode).To(Equal("KMAL"))
Expect(fplMessage.PerformanceCategory).To(Equal("C"))
Expect(fplMessage.RerouteInformation).To(Equal("FRT N640 ZBYN"))
Expect(fplMessage.Remarks).To(Equal("TCAS EQUIPPED"))
})
})
Context("with CNL body", func() {
// parser := NewBodyParser()
It("should parse the body correctly", func() {
body := "(CNL-YZR7979-ZSPD-ZBTJ)"
parser := NewBodyParser(body)
category, parsedBody, err := parser.Parse()
Expect(err).ToNot(HaveOccurred())
Expect(parsedBody).ToNot(BeNil())
Expect(category).To(Equal("CNL"))
Expect(parsedBody).To(BeAssignableToTypeOf(&domain.CNL{}))
cnlMessage := parsedBody.(*domain.CNL)
Expect(cnlMessage.AircraftID).To(Equal("YZR7979"))
})
})
Context("with DLA body", func() {
It("should parse the body correctly", func() {
body := "(DLA-CSN3133-ZGGG0110-ZBTJ)"
parser := NewBodyParser(body)
category, parsedBody, err := parser.Parse()
Expect(err).ToNot(HaveOccurred())
Expect(parsedBody).ToNot(BeNil())
Expect(category).To(Equal("DLA"))
Expect(parsedBody).To(BeAssignableToTypeOf(&domain.DLA{}))
dlaMessage := parsedBody.(*domain.DLA)
Expect(dlaMessage.AircraftID).To(Equal("CSN3133"))
Expect(dlaMessage.DepartureAirport).To(Equal("ZGGG"))
Expect(dlaMessage.NewDepartureTime).To(Equal("0110"))
Expect(dlaMessage.ArrivalAirport).To(Equal("ZBTJ"))
})
})
})
Describe("Parse whole real message", func() {
Context("with a real ARR message", func() {
message := `
ZCZC TMQ2526 141605
FF ZBTJZPZX
141604 ZBACZQZX
(ARR-JAE7433/A0132-RKSI-ZBTJ1604)
NNNN
`
It("should parse the whole message correctly", func() {
parsedMessage, err := Parse(message)
Expect(err).ToNot(HaveOccurred())
Expect(parsedMessage).ToNot(BeNil())
Expect(parsedMessage.Parsed).To(BeTrue())
Expect(parsedMessage.MessageID).To(Equal("TMQ2526"))
Expect(parsedMessage.DateTime).To(Equal("141605"))
Expect(parsedMessage.PrimaryAddress).To(Equal("ZBTJZPZX"))
Expect(parsedMessage.SecondaryAddresses).To(Equal(""))
Expect(parsedMessage.PriorityIndicator).To(Equal("FF"))
Expect(parsedMessage.OriginatorDateTime).To(Equal("141604"))
Expect(parsedMessage.Originator).To(Equal("ZBACZQZX"))
arrmsg := parsedMessage.BodyData.(*domain.ARR)
Expect(arrmsg.Category).To(Equal("ARR"))
Expect(arrmsg.AircraftID).To(Equal("JAE7433"))
Expect(arrmsg.SSRModeAndCode).To(Equal("A0132"))
Expect(arrmsg.DepartureAirport).To(Equal("RKSI"))
Expect(arrmsg.ArrivalAirport).To(Equal("ZBTJ"))
Expect(arrmsg.ArrivalTime).To(Equal("1604"))
})
})
Context("with this real FPL message", func() {
message := `ZCZC TMQ2617 142150
GG ZBTJZPZX
150551 ZBTJUOBK
(FPL-OKA2861-IS
-MA60/M-SHID/C
-ZBTJ0030
-K0420S0450 CG J1 FZ
-ZSYT0100 ZSQD ZYTL
-REG/B3710 SEL/ RMK/TCAS )
NNNN
`
It("should parse the whole message correctly", func() {
parsedMessage, err := Parse(message)
Expect(err).ToNot(HaveOccurred())
Expect(parsedMessage).ToNot(BeNil())
Expect(parsedMessage.Parsed).To(BeTrue())
Expect(parsedMessage.MessageID).To(Equal("TMQ2617"))
Expect(parsedMessage.DateTime).To(Equal("142150"))
Expect(parsedMessage.PrimaryAddress).To(Equal("ZBTJZPZX"))
Expect(parsedMessage.SecondaryAddresses).To(Equal(""))
Expect(parsedMessage.PriorityIndicator).To(Equal("GG"))
Expect(parsedMessage.OriginatorDateTime).To(Equal("150551"))
Expect(parsedMessage.Originator).To(Equal("ZBTJUOBK"))
fplmsg := parsedMessage.BodyData.(*domain.FPL)
Expect(fplmsg.Category).To(Equal("FPL"))
Expect(fplmsg.FlightNumber).To(Equal("OKA2861"))
Expect(fplmsg.FlightRulesAndType).To(Equal("IS"))
Expect(fplmsg.AircraftID).To(Equal("MA60/M"))
Expect(fplmsg.SSRModeAndCode).To(Equal("SHID/C"))
Expect(fplmsg.DepartureAirport).To(Equal("ZBTJ"))
Expect(fplmsg.DepartureTime).To(Equal("0030"))
Expect(fplmsg.CruisingSpeedAndLevel).To(Equal("K0420S0450"))
Expect(fplmsg.Route).To(Equal("CG J1 FZ"))
Expect(fplmsg.DestinationAndTotalTime).To(Equal("ZSYT0100"))
Expect(fplmsg.AlternateAirport).To(Equal("ZSQD ZYTL"))
Expect(fplmsg.OtherInfo).To(Equal("REG/B3710 SEL/ RMK/TCAS"))
// Expect(fplmsg.PBN).To(Equal("B3710"))
Expect(fplmsg.SELCALCode).To(Equal(""))
Expect(fplmsg.Remarks).To(Equal("TCAS"))
})
})
})
Describe("Utility Functions", func() {
It("should clean text correctly", func() {
text := `ZCZC TMQ2530 141614
1234
4567
NNNN`
expect := "ZCZC TMQ2530 141614\n1234\n 4567"
cleaned := cleanMessage(text)
Expect(cleaned).To(Equal(expect))
})
It("should parse start indicator correctly", func() {
line := "ZCZC TMQ2530 141614"
startIndicator, messageID, dateTime, err := parseStartIndicator(line)
Expect(err).ToNot(HaveOccurred())
Expect(startIndicator).To(Equal("ZCZC"))
Expect(messageID).To(Equal("TMQ2530"))
Expect(dateTime).To(Equal("141614"))
})
It("should return error for invalid start indicator line", func() {
line := "Invalid Line"
_, _, _, err := parseStartIndicator(line)
Expect(err).To(HaveOccurred())
})
It("should parse priority and primary address correctly", func() {
line := "QU TSNZPCA"
priority, primary := parsePriorityAndPrimary(line)
Expect(priority).To(Equal("QU"))
Expect(primary).To(Equal("TSNZPCA"))
})
It("should return empty strings for invalid priority and primary address line", func() {
line := "Invalid-Line"
priority, primary := parsePriorityAndPrimary(line)
Expect(priority).To(BeEmpty())
Expect(primary).To(BeEmpty())
})
It("should parse remaining lines correctly", func() {
lines := []string{"QU PEKUDCA TSNUOCA TSNZPCA TSNUFCA", ".SELOZKE 170999", "BEGIN PART 01"}
secondaryAddresses, originator, originatorDateTime, bodyAndFooter := parseRemainingLines(lines)
Expect(secondaryAddresses).To(Equal(" QU PEKUDCA TSNUOCA TSNZPCA TSNUFCA"))
Expect(originator).To(Equal("SELOZKE"))
Expect(originatorDateTime).To(Equal("170999"))
Expect(bodyAndFooter).To(Equal("BEGIN PART 01\n"))
})
})
})
+72
View File
@@ -0,0 +1,72 @@
package parser
import "regexp"
// String constants
const (
StartIndicatorPrefix = "ZCZC"
EndHeaderMarker = "."
BeginPartMarker = "BEGIN PART"
Category = "category"
CategoryArrival = "ARR"
CategoryDeparture = "DEP"
CategoryCancellation = "CNL"
CategoryDelay = "DLA"
CategoryFlightPlan = "FPL"
CANCELLED = "CNL"
AirportCode = "airport"
Date = "date"
Task = "task"
Index = "idx"
FlightNumber = "number"
Register = "reg"
)
// Regular expression patterns
const (
AllDigitsPattern = `^(?P<dep_time>\d+)$`
IndexPattern = `^(?P<idx>\(?L?[0-9]+\)?:?\.?)$`
DatePattern = `^(?P<date>\d{2}\w{3})$`
TaskPattern = `(?P<task>[A-Z]\/[A-Z])$`
WaypointPattern = `^(SI:)?(?P<arr_time>\d{4}(\(\d{2}[A-Z]{3}\))?)?\/?(?P<airport>[A-Z]{3})\/?(?P<dep_time>\d{4}(\(\d{2}[A-Z]{3}\))?)?$`
FlightNumberPattern = `^(?P<number>[0-9A-Z][0-9A-Z]\d{3,5}(\/\d+)*)$`
RegisterPattern = `^(?P<reg>B\d{4})$`
ArrPatternString = `^\((?P<category>[A-Z]{3})-(?P<number>[A-Z0-9]+)(\/?(?P<ssr>[A-Z0-9]+))?-(?P<dep>[A-Z]{4})-(?P<arr>[A-Z]{4})(?P<arr_time>\d{4})\)$`
DepPatternString = `^\((?P<category>[A-Z]{3})-(?P<number>[A-Z0-9]+)(\/(?P<ssr>[A-Z0-9]+))?-(?P<dep>[A-Z]{4})(?P<dep_time>\d{4})-(?P<arr>[A-Z]{4})\)$`
FplPatternString = `\((?P<category>[A-Z]{3})-(?P<number>[A-Z]+\d+)-(?P<indicator>[A-Z]{2})\n-(?P<aircraft>[A-Z]+\d+\/?[A-Z]?)\n?-(?P<surve>.*)\n?-(?P<dep>[A-Z]{4})(?P<dep_time>\d{4})\n?-(?P<speed>[A-Z]+\d+)(?P<level>[A-Z0-9]+)\s+(?P<route>(.|\n)+)\n-(?P<dest>[A-Z]{4})(?P<estt>\d{4})\s?(?P<alter>(\s[A-Z]{4})+)\n?-([A-Z]{3}\/(?:[A-Z]{4}\d{4}\s?)+)?(?P<other>(?m)[A-Z]{3}\/(.|\n)*)\)$`
CnlPatternString = `^\((?P<category>[A-Z]{3})-(?P<number>\w+\d+)-?(?P<dep>[A-Z]{4})?-?(?<arr>[A-Z]{4})\)$`
DlaPatternString = `^\((?P<category>[A-Z]{3})-(?P<number>\w+\d+)-?(?P<dep>[A-Z]{4})(?P<dep_time>\d{4})?-?(?<arr>[A-Z]{4})(?<arr_time>\d{4})?\)$`
)
// Compiled regular expressions
var (
AllDigitsExpression = regexp.MustCompile(AllDigitsPattern)
IndexExpression = regexp.MustCompile(IndexPattern)
TaskExpression = regexp.MustCompile(TaskPattern)
DateExpression = regexp.MustCompile(DatePattern)
WaypointExpression = regexp.MustCompile(WaypointPattern)
FlightNumberExpression = regexp.MustCompile(FlightNumberPattern)
RegisterExpression = regexp.MustCompile(RegisterPattern)
ArrPatternExpression = regexp.MustCompile(ArrPatternString)
DepPatternExpression = regexp.MustCompile(DepPatternString)
FplPatternExpression = regexp.MustCompile(FplPatternString)
CnlPatternExpression = regexp.MustCompile(CnlPatternString)
DlaPatternExpression = regexp.MustCompile(DlaPatternString)
BodyTypePattern = regexp.MustCompile(`^\(([A-Z]{3})(.*\n?)+\)$`)
categoryRegex = regexp.MustCompile(`\((?P<category>[A-Z]+)-`)
emptyLineRemove = regexp.MustCompile(`(?m)^\s*$`)
bodyOnly = regexp.MustCompile(`(.|\n)?(ZCZC(.|\n)*)NNNN(.|\n)?$`)
originator = regexp.MustCompile(`(?P<originatorDateTime>[0-9]+)\s(?P<originator>[A-Z]+)`)
navPattern = regexp.MustCompile(`(?m)NAV\/(?P<nav>\w+)`)
remarkPattern = regexp.MustCompile(`(?s)RMK\/(?P<remark>.*)`)
selPattern = regexp.MustCompile(`(?m)SEL\/(?P<sel>\w+)`)
regPattern = regexp.MustCompile(`(?m)REG\/(?P<reg>[A-Z0-9]+)`)
pbnPattern = regexp.MustCompile(`(?m)PBN\/(?P<pbn>[A-Z0-9]+)`)
eetPattern = regexp.MustCompile(`(?s)(-?EET\/(?P<eet>(?:[A-Z]{4}\d{4}\s*)+))`)
performancePattern = regexp.MustCompile(`(?s)-?PER\/(?P<per>\w)`)
reroutePattern = regexp.MustCompile(`(?m)RIF\/(?P<rif>.*)[A-Z]{3}\/`)
)
+2 -2
View File
@@ -1,9 +1,9 @@
package parser
import "caatsm/internal/model"
import "caatsm/internal/adapter/dto"
// Parser defines the interface for parsing raw telegram messages
type Parser interface {
// Parse parses a raw message string and returns a ParsedTelegram
Parse(rawText string) (*model.ParsedTelegram, error)
Parse(rawText string) (*dto.ParsedTelegram, error)
}
+327
View File
@@ -0,0 +1,327 @@
package parser
import (
"regexp"
)
// BodyConfig represents the configuration for parsing message bodies.
type BodyConfig struct {
Patterns []PatternConfig
}
// PatternConfig represents the configuration for a specific pattern.
type PatternConfig struct {
Pattern string
Comments string
Expression *regexp.Regexp
}
// LineParser represents a line parser configuration.
type LineParser struct {
Airlines []string
MinLen int
WaypointStart int
Fields map[int]string
}
var (
bodyPatterns = map[string]BodyConfig{}
parserMap = map[string]*regexp.Regexp{}
parserDef = &[]LineParser{}
)
func init() {
// Initialize body patterns.
bodyPatterns = map[string]BodyConfig{
"ARR": {
Patterns: []PatternConfig{
{
Pattern: ArrPatternString,
Comments: "Pattern for ARR message",
Expression: ArrPatternExpression,
},
},
},
"DEP": {
Patterns: []PatternConfig{
{
Pattern: DepPatternString,
Comments: "Pattern for DEP message",
Expression: DepPatternExpression,
},
},
},
"FPL": {
Patterns: []PatternConfig{
{
Pattern: FplPatternString,
Comments: "Pattern for FPL message",
Expression: FplPatternExpression,
},
},
},
"CNL": {
Patterns: []PatternConfig{
{
Pattern: CnlPatternString,
Comments: "Pattern for CNL message",
Expression: CnlPatternExpression,
},
},
},
"DLA": {
Patterns: []PatternConfig{
{
Pattern: DlaPatternString,
Comments: "Pattern for DLA message",
Expression: DlaPatternExpression,
},
},
},
}
// Initialize parser map.
parserMap = map[string]*regexp.Regexp{
Index: IndexExpression,
Task: TaskExpression,
Date: DateExpression,
FlightNumber: FlightNumberExpression,
Register: RegisterExpression,
}
// Initialize parser definitions.
parserDef = &[]LineParser{
{
Airlines: []string{"FM"},
MinLen: 6,
WaypointStart: 5,
Fields: map[int]string{
0: Task,
1: FlightNumber,
2: Register,
},
},
{
Airlines: []string{"MF"},
MinLen: 5,
WaypointStart: 4,
Fields: map[int]string{
0: Index,
1: FlightNumber,
2: Register,
},
},
{
Airlines: []string{"8X"},
MinLen: 9,
WaypointStart: 7,
Fields: map[int]string{
0: Index,
1: Date,
2: FlightNumber,
3: Register,
},
},
{
Airlines: []string{"HU"},
MinLen: 6,
WaypointStart: 5,
Fields: map[int]string{
0: Index,
1: Task,
2: FlightNumber,
3: Register,
},
},
{
Airlines: []string{"JD"},
MinLen: 7,
WaypointStart: 5,
Fields: map[int]string{
0: Index,
1: FlightNumber,
2: Register,
},
},
{
Airlines: []string{"GS"},
MinLen: 4,
WaypointStart: 3,
Fields: map[int]string{
0: Index,
1: FlightNumber,
2: Register,
},
},
{
Airlines: []string{"Y8"},
MinLen: 6,
WaypointStart: 3,
Fields: map[int]string{
0: Index,
1: FlightNumber,
2: Register,
},
},
{
Airlines: []string{"3U"},
MinLen: 8,
WaypointStart: 6,
Fields: map[int]string{
0: Index,
1: Date,
2: FlightNumber,
3: Register,
},
},
{
Airlines: []string{"CK"},
MinLen: 4,
WaypointStart: 3,
Fields: map[int]string{
0: Task,
1: FlightNumber,
2: Register,
},
},
{
Airlines: []string{"G5"},
MinLen: 8,
WaypointStart: 5,
Fields: map[int]string{
0: Index,
1: Task,
2: FlightNumber,
3: Register,
},
},
{
Airlines: []string{"9C"},
MinLen: 8,
WaypointStart: 6,
Fields: map[int]string{
0: Date,
1: Task,
2: FlightNumber,
3: Register,
},
},
{
Airlines: []string{"ZH"},
MinLen: 9,
WaypointStart: 7,
Fields: map[int]string{
0: Index,
1: Task,
2: Date,
3: FlightNumber,
4: Register,
},
},
{
Airlines: []string{"8L"},
MinLen: 6,
WaypointStart: 4,
Fields: map[int]string{
0: Index,
1: Task,
2: FlightNumber,
3: Register,
},
},
{
Airlines: []string{"SC"},
MinLen: 9,
WaypointStart: 7,
Fields: map[int]string{
0: Index,
1: FlightNumber,
2: Register,
},
},
{
Airlines: []string{"PN"},
MinLen: 7,
WaypointStart: 5,
Fields: map[int]string{
0: Index,
1: FlightNumber,
2: Register,
},
},
{
Airlines: []string{"CZ"},
MinLen: 6,
WaypointStart: 4,
Fields: map[int]string{
0: Index,
1: FlightNumber,
2: Register,
},
},
{
Airlines: []string{"HO"},
MinLen: 7,
WaypointStart: 6,
Fields: map[int]string{
0: Index,
1: Date,
2: Task,
3: FlightNumber,
4: Register,
},
},
{
Airlines: []string{"NS"},
MinLen: 7,
WaypointStart: 6,
Fields: map[int]string{
0: Index,
1: Date,
2: Task,
3: FlightNumber,
4: Register,
},
},
{
Airlines: []string{"EU"},
MinLen: 7,
WaypointStart: 6,
Fields: map[int]string{
0: Task,
1: Date,
2: FlightNumber,
3: Register,
},
},
}
}
// FindPatterns finds the matching body configuration based on the message body.
func FindPatterns(messageBody string) *BodyConfig {
if match := BodyTypePattern.FindStringSubmatch(messageBody); len(match) > 1 {
name := match[1]
if bodyConfig, found := bodyPatterns[name]; found {
return &bodyConfig
}
}
return nil
}
// ParseBody parses the message body and returns the extracted values.
func ParseBody(messageBody string) map[string]string {
if body := FindPatterns(messageBody); body != nil {
for _, pattern := range body.Patterns {
if matches := pattern.Expression.FindStringSubmatch(messageBody); matches != nil {
result := make(map[string]string)
for i, name := range pattern.Expression.SubexpNames() {
if i != 0 && name != "" {
result[name] = matches[i]
}
}
return result
}
}
}
return nil
}
+43
View File
@@ -0,0 +1,43 @@
package parser
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Pattern Parser", func() {
Describe("FindPatterns", func() {
It("should return the correct BodyConfig based on the message body", func() {
message := "(ARR-AB123-SSR1234-KJFK-KLAX)"
bodyConfig := FindPatterns(message)
Expect(bodyConfig).NotTo(BeNil())
// Expect(bodyConfig.Name).To(Equal("ARR"))
})
It("should return nil if no pattern matches", func() {
message := "(XYZ-123)"
bodyConfig := FindPatterns(message)
Expect(bodyConfig).To(BeNil())
})
})
Describe("ParseBody", func() {
It("should parse the message body and extract data based on patterns", func() {
message := "(ARR-AB123/A1234-KJFK-KLAX1234)"
parsedData := ParseBody(message)
Expect(parsedData).NotTo(BeNil())
Expect(parsedData["category"]).To(Equal("ARR"))
Expect(parsedData["number"]).To(Equal("AB123"))
Expect(parsedData["ssr"]).To(Equal("A1234"))
Expect(parsedData[DepartureCode]).To(Equal("KJFK"))
Expect(parsedData[ArrivalCode]).To(Equal("KLAX"))
})
It("should return nil if no patterns match", func() {
message := "(XYZ-123)"
parsedData := ParseBody(message)
Expect(parsedData).To(BeNil())
})
})
})
+11 -1
View File
@@ -1,7 +1,17 @@
package parser
import "caatsm/internal/adapter/dto"
// AviationParser implements the Parser interface
type AviationParser struct{}
// Parse parses a raw message string and returns a ParsedTelegram
func (p *AviationParser) Parse(rawText string) (*dto.ParsedTelegram, error) {
return Parse(rawText)
}
// ProvideParser creates a parser instance
func ProvideParser() Parser {
return NewAviationParser()
return &AviationParser{}
}
+221
View File
@@ -0,0 +1,221 @@
package parser
import (
"caatsm/internal/domain"
"errors"
"strings"
"go.uber.org/zap"
)
func ExtractWaypoint(message string) *domain.WayPoint {
matches := WaypointExpression.FindStringSubmatch(message)
if matches == nil {
return nil
}
data := make(map[string]string)
for i, name := range WaypointExpression.SubexpNames() {
if i != 0 && name != "" {
data[name] = matches[i]
}
}
result := &domain.WayPoint{
ArrivalTime: data[ArrivalTime],
Airport: data[AirportCode],
DepartureTime: data[DepartureTime],
}
return result
}
func FindDef(code string) *LineParser {
// fmt.Printf("Finding definition for %s\n", code)
// fmt.Println("ParserDef: ", parserDef)
for _, def := range *parserDef {
for _, airline := range def.Airlines {
if airline == code {
return &def
}
}
}
return nil
}
func standardizeSpaces(s string) string {
return strings.Join(strings.Fields(s), " ")
}
func ParseWithDef(line string, parserDef *LineParser) *domain.ScheduleLine {
log := zap.S()
cleanLine := standardizeSpaces(strings.TrimSpace(line))
words := strings.Split(cleanLine, " ")
var flightSchedule = &domain.ScheduleLine{
Reference: line,
}
if strings.Contains(line, CANCELLED) {
flightSchedule.Comments = "Cancelled"
return flightSchedule
}
// var result map[string]string
if parserDef == nil {
log.Warnf("No definition found: %s", line)
flightSchedule.Comments = "No definition found [" + line + "] "
return flightSchedule
}
if len(words) < parserDef.MinLen {
log.Warnf("Line too short: %s", line)
flightSchedule.Comments = "Line too short [" + line + "] "
return flightSchedule
}
for i, field := range parserDef.Fields {
// log.Debugf("Parsing field %v -> %s", i, field)
data := extract(words[i], parserMap[field])
if data != nil {
switch field {
case Index:
flightSchedule.Index = data[Index]
case Date:
flightSchedule.Date = data[Date]
case Task:
flightSchedule.Task = data[Task]
case FlightNumber:
flightSchedule.FlightNumber = getFlightNumbers(data[FlightNumber])
case Register:
flightSchedule.AircraftReg = data[Register]
}
}
}
if len(words) > parserDef.WaypointStart {
flightSchedule.Waypoints, _ = parseWaypoints(words[parserDef.WaypointStart:])
} else {
log.Warn("No waypoints found")
flightSchedule.Comments = "No waypoints found"
}
return flightSchedule
}
// ParseLine processes a single line of schedule data and returns a ScheduleLine object.
func ParseLine(line string) (*domain.ScheduleLine, error) {
log := zap.S()
cleanLine := strings.TrimSpace(line)
words := strings.Split(cleanLine, " ")
flightSchedule := &domain.ScheduleLine{Reference: line}
if indexData := extract(words[0], IndexExpression); indexData != nil {
flightSchedule.Index = indexData[Index]
words = words[1:]
}
parseStrategy := []string{Task, Date, FlightNumber, Register}
_, maxParsed, err := parseFields(words, parseStrategy, flightSchedule)
if err != nil {
return nil, err
}
// Check if there are any waypoints after the parsed fields
if maxParsed+1 < len(words) {
waypoints, err := parseWaypoints(words[maxParsed+1:])
if err != nil {
return nil, err
}
flightSchedule.Waypoints = waypoints
} else {
log.Warn("No waypoints found")
flightSchedule.Comments = "No waypoints found"
}
return flightSchedule, nil
}
// parseFields processes the fields based on the given strategy and updates the flight schedule.
func parseFields(words []string, parseStrategy []string, flightSchedule *domain.ScheduleLine) (map[string]bool, int, error) {
parsed := make(map[string]bool)
var maxParsed int
for i, word := range words {
for _, name := range parseStrategy {
if parsed[name] {
continue
}
if data := extract(word, parserMap[name]); data != nil {
updateFlightSchedule(flightSchedule, name, data)
parsed[name] = true
maxParsed = i
break
}
}
}
return parsed, maxParsed, nil
}
// updateFlightSchedule updates the flight schedule based on the parsed data.
func updateFlightSchedule(flightSchedule *domain.ScheduleLine, name string, data map[string]string) {
switch name {
case Task:
flightSchedule.Task = data[Task]
case Date:
flightSchedule.Date = data[Date]
case FlightNumber:
flightSchedule.FlightNumber = getFlightNumbers(data[FlightNumber])
case Register:
flightSchedule.AircraftReg = data[Register]
}
}
// parseWaypoints processes a slice of waypoint strings and returns a slice of WayPoint objects.
func parseWaypoints(target []string) ([]domain.WayPoint, error) {
log := zap.S()
points := getValidPoints(target)
if len(points) == 0 {
log.Warn("No waypoints found")
return nil, errors.New("no waypoints")
}
var waypoints []domain.WayPoint
for i, point := range points {
if digits := extract(point, AllDigitsExpression); i > 0 && digits != nil && len(waypoints) > 0 {
waypoints[len(waypoints)-1].DepartureTime = digits[DepartureTime]
} else if waypoint := ExtractWaypoint(point); waypoint != nil {
waypoints = append(waypoints, *waypoint)
}
}
if len(waypoints) == 0 {
log.Warn("No waypoints found")
return nil, errors.New("no waypoints")
}
return waypoints, nil
}
func getValidPoints(data []string) []string {
var points []string
for i, point := range data {
if extract(point, WaypointExpression) != nil {
return data[i:]
}
}
return points
}
/**
* CZ6794/79
* CZ3301/2
* CA1371/1372/1527
*/
func getFlightNumbers(data string) []string {
if strings.Contains(data, "/") {
data := strings.Split(data, "/")
baseNumber := data[0]
baseLength := len(baseNumber)
flightNumbers := append([]string{}, baseNumber)
for _, number := range data[1:] {
length := len(number)
flightNumber := baseNumber[:baseLength-length] + number
flightNumbers = append(flightNumbers, flightNumber)
}
return flightNumbers
} else {
return []string{data}
}
}
@@ -0,0 +1,591 @@
package parser
import (
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Schedule Parser", func() {
Describe("Index Parser", func() {
Context("parse : 83.", func() {
It("should return a valid index", func() {
message := "83."
data := extract(message, IndexExpression)
Expect(data).NotTo(BeNil())
Expect(data[Index]).To(Equal("83."))
})
})
Context("parse : (21)", func() {
It("should return a valid index", func() {
message := "(21)"
data := extract(message, IndexExpression)
Expect(data).NotTo(BeNil())
Expect(data[Index]).To(Equal("(21)"))
})
})
Context("parse : L59", func() {
It("should return a valid index", func() {
message := "L59"
data := extract(message, IndexExpression)
Expect(data).NotTo(BeNil())
Expect(data[Index]).To(Equal("L59"))
})
})
Context("parse : (205)", func() {
It("should return a valid index", func() {
message := "(205)"
data := extract(message, IndexExpression)
Expect(data).NotTo(BeNil())
Expect(data[Index]).To(Equal("(205)"))
})
})
Context("parse : L01", func() {
It("should return a valid index", func() {
message := "L01"
data := extract(message, IndexExpression)
Expect(data).NotTo(BeNil())
Expect(data[Index]).To(Equal("L01"))
})
})
Context("parse : 01)", func() {
It("should return a valid index", func() {
message := "01)"
data := extract(message, IndexExpression)
Expect(data).NotTo(BeNil())
Expect(data[Index]).To(Equal("01)"))
})
})
})
Describe("Date Parser", func() {
Context("parse : 31OCT", func() {
message := "31OCT"
data := extract(message, DateExpression)
It("should return a valid date", func() {
Expect(data).NotTo(BeNil())
Expect(data[Date]).To(Equal("31OCT"))
})
})
})
Describe("Flight Number Parser", func() {
Context("parse : FM9134", func() {
It("should return a valid flight number", func() {
message := "FM9134"
data := extract(message, FlightNumberExpression)
Expect(data).NotTo(BeNil())
Expect(data[FlightNumber]).To(Equal("FM9134"))
})
})
Context("parse : Y87969", func() {
It("should return a valid flight number", func() {
message := "Y87969"
data := extract(message, FlightNumberExpression)
Expect(data).NotTo(BeNil())
Expect(data[FlightNumber]).To(Equal("Y87969"))
})
})
Context("parse : CK261", func() {
It("should return a valid flight number", func() {
message := "CK261"
data := extract(message, FlightNumberExpression)
Expect(data).NotTo(BeNil())
Expect(data[FlightNumber]).To(Equal("CK261"))
})
})
Context("parse : 9C8812", func() {
It("should return a valid flight number", func() {
message := "9C8812"
data := extract(message, FlightNumberExpression)
Expect(data).NotTo(BeNil())
Expect(data[FlightNumber]).To(Equal("9C8812"))
})
})
Context("CA1371/1372/1527", func() {
It("3 number : CA1371 CA1372 CA1527", func() {
message := "CA1371/1372/1527"
data := getFlightNumbers(message)
Expect(data).NotTo(BeNil())
Expect(data).To(HaveLen(3))
Expect(data).To(ContainElement("CA1371"))
Expect(data).To(ContainElement("CA1372"))
Expect(data).To(ContainElement("CA1527"))
})
})
Context("CZ3301/2", func() {
It("2 number : CZ3301 CZ3302", func() {
message := "CZ3301/2"
data := getFlightNumbers(message)
Expect(data).NotTo(BeNil())
Expect(data).To(HaveLen(2))
Expect(data).To(ContainElement("CZ3301"))
Expect(data).To(ContainElement("CZ3302"))
})
})
})
Describe("Schedule Date Parser", func() {
Context("parse : 29OCT", func() {
It("should return a valid date", func() {
message := "29OCT"
data := extract(message, DateExpression)
Expect(data).NotTo(BeNil())
Expect(data[Date]).To(Equal("29OCT"))
})
})
})
Describe("FindWaypoint", func() {
It("should return the correct waypoints based on the message", func() {
message := "1845(11JUN)TSN/2100"
waypoint := ExtractWaypoint(message)
Expect(waypoint).NotTo(BeNil())
Expect(waypoint.ArrivalTime).To(Equal("1845(11JUN)"))
Expect(waypoint.Airport).To(Equal("TSN"))
Expect(waypoint.DepartureTime).To(Equal("2100"))
})
It("should return nil if no waypoints are found", func() {
message := "18451TSN"
waypoints := ExtractWaypoint(message)
Expect(waypoints).To(BeNil())
})
It("TSN/0645", func() {
message := "TSN/0645"
waypoint := ExtractWaypoint(message)
Expect(waypoint).NotTo(BeNil())
Expect(waypoint.Airport).To(Equal("TSN"))
Expect(waypoint.DepartureTime).To(Equal("0645"))
})
})
Describe("Waypoints", func() {
Context("XIY/0415 TSN/0645 CGQ", func() {
It("should return 3 waypoints", func() {
points := strings.Split("XIY/0415 TSN/0645 CGQ", " ")
waypoints, err := parseWaypoints(points)
Expect(Expect(err).NotTo(HaveOccurred()))
Expect(waypoints).NotTo(BeNil())
Expect(waypoints).To(HaveLen(3))
Expect(waypoints[0].Airport).To(Equal("XIY"))
Expect(waypoints[0].DepartureTime).To(Equal("0415"))
Expect(waypoints[1].Airport).To(Equal("TSN"))
Expect(waypoints[1].DepartureTime).To(Equal("0645"))
Expect(waypoints[2].Airport).To(Equal("CGQ"))
})
})
Context("ICN 0235 TSN", func() {
It("should return 2 waypoints", func() {
points := strings.Split("ICN 0235 TSN", " ")
waypoints, err := parseWaypoints(points)
Expect(Expect(err).NotTo(HaveOccurred()))
Expect(waypoints).NotTo(BeNil())
Expect(waypoints).To(HaveLen(2))
Expect(waypoints[0].Airport).To(Equal("ICN"))
Expect(waypoints[0].DepartureTime).To(Equal("0235"))
Expect(waypoints[1].Airport).To(Equal("TSN"))
})
})
})
})
var _ = Describe("Parser Definition", func() {
Context("MF", func() {
It("valid def", func() {
def := FindDef("MF")
Expect(def).NotTo(BeNil())
Expect(def.Airlines).To(ContainElement("MF"))
})
})
Context("FM", func() {
It("valid def", func() {
def := FindDef("FM")
Expect(def).NotTo(BeNil())
Expect(def.Airlines).To(ContainElement("FM"))
})
})
Context("8X", func() {
It("valid def", func() {
def := FindDef("8X")
Expect(def).NotTo(BeNil())
Expect(def.Airlines).To(ContainElement("8X"))
})
})
Context("XX", func() {
It("nil", func() {
def := FindDef("XX")
Expect(def).To(BeNil())
})
})
})
var _ = Describe("Parse Line with PreDef", func() {
Context("FM", func() {
It("W/Z FM9134 B2688 1/1ILS (00) TSN0100 SHA", func() {
lineText := "W/Z FM9134 B2688 1/1ILS (00) TSN0100 SHA"
def := FindDef("FM")
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Task).To(Equal("W/Z"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("FM9134"))
Expect(schedule.AircraftReg).To(Equal("B2688"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0100"))
Expect(schedule.Waypoints[1].Airport).To(Equal("SHA"))
})
})
Context("MF", func() {
It("01) MF8193 B5595 ILS(8) HGH1100 1305TSN", func() {
lineText := "01) MF8193 B5595 ILS(8) HGH1100 1305TSN"
def := FindDef("MF")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("01)"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("MF8193"))
Expect(schedule.AircraftReg).To(Equal("B5595"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("HGH"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("1100"))
Expect(schedule.Waypoints[1].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[1].ArrivalTime).To(Equal("1305"))
})
})
Context("8X", func() {
It("L1: 29OCT BK2735 B2863 ILS IS (3/6) TSN2350(28OCT) HAK", func() {
lineText := "L1: 29OCT BK2735 B2863 ILS IS (3/6) TSN2350(28OCT) HAK"
def := FindDef("8X")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("L1:"))
Expect(schedule.Date).To(Equal("29OCT"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("BK2735"))
Expect(schedule.AircraftReg).To(Equal("B2863"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("2350(28OCT)"))
Expect(schedule.Waypoints[1].Airport).To(Equal("HAK"))
})
})
Context("HU", func() {
It("L05 W/Z HU7205 B5406 (9) TSN/2355(30OCT) PVG", func() {
lineText := "L05 W/Z HU7205 B5406 (9) TSN/2355(30OCT) PVG"
def := FindDef("HU")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("L05"))
Expect(schedule.Task).To(Equal("W/Z"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("HU7205"))
Expect(schedule.AircraftReg).To(Equal("B5406"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("2355(30OCT)"))
Expect(schedule.Waypoints[1].Airport).To(Equal("PVG"))
})
})
Context("JD", func() {
It("1) JD5195 B6727 ILS I(9) SYX/0800 1135/TSN", func() {
lineText := "1) JD5195 B6727 ILS I(9) SYX/0800 1135/TSN"
def := FindDef("JD")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("1)"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("JD5195"))
Expect(schedule.AircraftReg).To(Equal("B6727"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("SYX"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0800"))
Expect(schedule.Waypoints[1].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[1].ArrivalTime).To(Equal("1135"))
})
})
Context("GS", func() {
It("01 GS7635 B3193 XIY0020(16APR) CGD", func() {
lineText := "01 GS7635 B3193 XIY0020(16APR) CGD"
def := FindDef("GS")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("01"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("GS7635"))
Expect(schedule.AircraftReg).To(Equal("B3193"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("XIY"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0020(16APR)"))
Expect(schedule.Waypoints[1].Airport).To(Equal("CGD"))
})
})
Context("Y8", func() {
It("13 Y87444 B2578 ICN 0235 TSN", func() {
lineText := "13 Y87444 B2578 ICN 0235 TSN"
def := FindDef("Y8")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("13"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("Y87444"))
Expect(schedule.AircraftReg).To(Equal("B2578"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("ICN"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0235"))
Expect(schedule.Waypoints[1].Airport).To(Equal("TSN"))
})
})
Context("3U", func() {
It("01) 31OCT 3U8863 B6598 CAT1 (10) CKG0010 0235TSN", func() {
lineText := "01) 31OCT 3U8863 B6598 CAT1 (10) CKG0010 0235TSN"
def := FindDef("3U")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("01)"))
Expect(schedule.Date).To(Equal("31OCT"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("3U8863"))
Expect(schedule.AircraftReg).To(Equal("B6598"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("CKG"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0010"))
Expect(schedule.Waypoints[1].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[1].ArrivalTime).To(Equal("0235"))
})
})
Context("CK", func() {
It("01)H/Z CK261 B2076 PVG1535(30OCT) 1705TPE", func() {
lineText := "01)H/Z CK261 B2076 PVG1535(30OCT) 1705TPE"
def := FindDef("CK")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Task).To(Equal("H/Z"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("CK261"))
Expect(schedule.AircraftReg).To(Equal("B2076"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("PVG"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("1535(30OCT)"))
Expect(schedule.Waypoints[1].Airport).To(Equal("TPE"))
Expect(schedule.Waypoints[1].ArrivalTime).To(Equal("1705"))
})
})
Context("G5", func() {
It("L01 W/Z G52665 B7762 (6) CKG/0725 CIH/0940 TSN", func() {
lineText := "L01 W/Z G52665 B7762 (6) CKG/0725 CIH/0940 TSN"
def := FindDef("G5")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("L01"))
Expect(schedule.Task).To(Equal("W/Z"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("G52665"))
Expect(schedule.AircraftReg).To(Equal("B7762"))
Expect(len(schedule.Waypoints)).To(Equal(3))
Expect(schedule.Waypoints[0].Airport).To(Equal("CKG"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0725"))
Expect(schedule.Waypoints[1].Airport).To(Equal("CIH"))
Expect(schedule.Waypoints[1].DepartureTime).To(Equal("0940"))
Expect(schedule.Waypoints[2].Airport).To(Equal("TSN"))
})
})
Context("9C", func() {
It("31OCT W/Z 9C8884 B6573 ILS1/1 (06) TSN0650 SYX", func() {
lineText := "31OCT W/Z 9C8884 B6573 ILS1/1 (06) TSN0650 SYX"
def := FindDef("9C")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Date).To(Equal("31OCT"))
Expect(schedule.Task).To(Equal("W/Z"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("9C8884"))
Expect(schedule.AircraftReg).To(Equal("B6573"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0650"))
Expect(schedule.Waypoints[1].Airport).To(Equal("SYX"))
})
})
Context("ZH", func() {
It("204) W/Z 31OCT ZH9783 B5670 CAT1 (10) SZX0045 0355TSN", func() {
lineText := "204) W/Z 31OCT ZH9783 B5670 CAT1 (10) SZX0045 0355TSN"
def := FindDef("ZH")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("204)"))
Expect(schedule.Task).To(Equal("W/Z"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("ZH9783"))
Expect(schedule.AircraftReg).To(Equal("B5670"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("SZX"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0045"))
Expect(schedule.Waypoints[1].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[1].ArrivalTime).To(Equal("0355"))
})
})
Context("8L", func() {
It("L59 W/Z 8L9976 B6959 TSN/0510 CTU/0855 KMG", func() {
lineText := "L59 W/Z 8L9976 B6959 TSN/0510 CTU/0855 KMG"
def := FindDef("8L")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("L59"))
Expect(schedule.Task).To(Equal("W/Z"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("8L9976"))
Expect(schedule.AircraftReg).To(Equal("B6959"))
Expect(len(schedule.Waypoints)).To(Equal(3))
Expect(schedule.Waypoints[0].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0510"))
Expect(schedule.Waypoints[1].Airport).To(Equal("CTU"))
Expect(schedule.Waypoints[1].DepartureTime).To(Equal("0855"))
Expect(schedule.Waypoints[2].Airport).To(Equal("KMG"))
})
})
Context("SC", func() {
It("(1) SC4717 B3080 CRJ7 ILS I (6) TAO/2350 TSN", func() {
lineText := "(1) SC4717 B3080 CRJ7 ILS I (6) TAO/2350 TSN"
def := FindDef("SC")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("(1)"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("SC4717"))
Expect(schedule.AircraftReg).To(Equal("B3080"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("TAO"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("2350"))
Expect(schedule.Waypoints[1].Airport).To(Equal("TSN"))
})
})
Context("CZ", func() {
It("83. CZ3301/2 B2823 B752 CAN0135 TSN0535 CAN", func() {
lineText := "83. CZ3301/2 B2823 B752 CAN0135 TSN0535 CAN"
def := FindDef("CZ")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("83."))
Expect(len(schedule.FlightNumber)).To(Equal(2))
Expect(schedule.FlightNumber).To(ContainElement("CZ3301"))
Expect(schedule.FlightNumber).To(ContainElement("CZ3302"))
Expect(schedule.AircraftReg).To(Equal("B2823"))
Expect(len(schedule.Waypoints)).To(Equal(3))
Expect(schedule.Waypoints[0].Airport).To(Equal("CAN"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0135"))
Expect(schedule.Waypoints[1].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[1].DepartureTime).To(Equal("0535"))
Expect(schedule.Waypoints[2].Airport).To(Equal("CAN"))
})
It("83. CZ3301/2 B2823 B752 CAN0135 TSN0535 CAN", func() {
lineText := "83. CZ3301/2 B2823 B752 CAN0135 TSN0535 CAN"
def := FindDef("CZ")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("83."))
Expect(len(schedule.FlightNumber)).To(Equal(2))
Expect(schedule.FlightNumber).To(ContainElement("CZ3301"))
Expect(schedule.FlightNumber).To(ContainElement("CZ3302"))
Expect(schedule.AircraftReg).To(Equal("B2823"))
Expect(len(schedule.Waypoints)).To(Equal(3))
Expect(schedule.Waypoints[0].Airport).To(Equal("CAN"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0135"))
Expect(schedule.Waypoints[1].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[1].DepartureTime).To(Equal("0535"))
Expect(schedule.Waypoints[2].Airport).To(Equal("CAN"))
})
})
Context("HO", func() {
It("124) 14NOV W/Z HO1245 B6966 ILS(8) SHA0005 0255CKG", func() {
lineText := "124) 14NOV W/Z HO1245 B6966 ILS(8) SHA0005 0255CKG"
def := FindDef("HO")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
Expect(schedule.Index).To(Equal("124)"))
Expect(schedule.Task).To(Equal("W/Z"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("HO1245"))
Expect(schedule.AircraftReg).To(Equal("B6966"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("SHA"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0005"))
Expect(schedule.Waypoints[1].Airport).To(Equal("CKG"))
Expect(schedule.Waypoints[1].ArrivalTime).To(Equal("0255"))
})
})
Context("EU", func() {
It("38)W/Z 27NOV EU2748 B6900 CAT1 (8) TSN0800 1055CTU", func() {
lineText := "38)W/Z 27NOV EU2748 B6900 CAT1 (8) TSN0800 1055CTU"
def := FindDef("EU")
Expect(def).NotTo(BeNil())
schedule := ParseWithDef(lineText, def)
Expect(schedule).NotTo(BeNil())
// Expect(schedule.Index).To(Equal("38)"))
Expect(schedule.Task).To(Equal("W/Z"))
Expect(len(schedule.FlightNumber)).To(Equal(1))
Expect(schedule.FlightNumber[0]).To(Equal("EU2748"))
Expect(schedule.AircraftReg).To(Equal("B6900"))
Expect(len(schedule.Waypoints)).To(Equal(2))
Expect(schedule.Waypoints[0].Airport).To(Equal("TSN"))
Expect(schedule.Waypoints[0].DepartureTime).To(Equal("0800"))
Expect(schedule.Waypoints[1].Airport).To(Equal("CTU"))
Expect(schedule.Waypoints[1].ArrivalTime).To(Equal("1055"))
})
})
})
+13
View File
@@ -0,0 +1,13 @@
package parser
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestParsers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Parsers Suite")
}
-8
View File
@@ -1,8 +0,0 @@
package adapter
// Publisher defines the interface for publishing parsed messages
type Publisher interface {
// Publish publishes a parsed message
Publish(message interface{}) error
}
-19
View File
@@ -1,19 +0,0 @@
package adapter
import (
"context"
"caatsm/internal/model"
)
// Repository defines the interface for message persistence
type Repository interface {
// InsertOne inserts a single telegram message
InsertOne(ctx context.Context, msg *model.ParsedTelegram) error
// InsertBatch inserts multiple telegram messages in a batch
InsertBatch(ctx context.Context, msgs []*model.ParsedTelegram) error
// InsertRaw captures an unparsed or failed telegram for later analysis.
InsertRaw(ctx context.Context, msg *model.ParsedTelegram) error
}