refactor parser packages
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
package aviation
|
||||
|
||||
import (
|
||||
"caatsm/internal/adapter/dto"
|
||||
"caatsm/internal/domain"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
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()
|
||||
copied := make(map[string]BodyConfig, len(parser.bodyPatterns))
|
||||
for k, v := range parser.bodyPatterns {
|
||||
copied[k] = v
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
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 headerToParsedTelegram(header Header) dto.ParsedTelegram {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
parsed := headerToParsedTelegram(header)
|
||||
parsed.Parsed = false
|
||||
parsed.Comments = bodyErr.Error()
|
||||
parsed.Status = dto.MessageStatusBodyError
|
||||
parsed.ErrorReason = bodyErr.Error()
|
||||
return &parsed, fmt.Errorf("%w: %w", ErrBodyParse, bodyErr)
|
||||
}
|
||||
|
||||
parsed := headerToParsedTelegram(header)
|
||||
parsed.BodyData = bodyData
|
||||
parsed.Parsed = true
|
||||
parsed.Status = dto.MessageStatusParsed
|
||||
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, "."):
|
||||
// Validate if dot-prefixed line matches originator format: .ORIGINATOR_CODE YYMMDD
|
||||
// Originator code should be uppercase letters, date/time should be digits
|
||||
originatorInfo := strings.Fields(line[1:])
|
||||
if len(originatorInfo) >= 2 {
|
||||
// Check if first token is all uppercase letters and second is all digits
|
||||
firstToken := originatorInfo[0]
|
||||
secondToken := originatorInfo[1]
|
||||
if isAllUppercaseLetters(firstToken) && isAllDigits(secondToken) {
|
||||
originator = firstToken
|
||||
originatorDateTime = secondToken
|
||||
headerEnded = true
|
||||
} else {
|
||||
// Doesn't match originator format, treat as body content
|
||||
headerEnded = true
|
||||
bodyAndFooter.WriteString(line + "\n")
|
||||
}
|
||||
} else {
|
||||
// Not enough tokens for originator format, treat as body content
|
||||
headerEnded = true
|
||||
bodyAndFooter.WriteString(line + "\n")
|
||||
}
|
||||
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 "", ""
|
||||
}
|
||||
|
||||
// isAllUppercaseLetters checks if a string contains only uppercase letters
|
||||
func isAllUppercaseLetters(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if r < 'A' || r > 'Z' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isAllDigits checks if a string contains only digits
|
||||
func isAllDigits(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package aviation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Sample messages for benchmarking
|
||||
var (
|
||||
benchARRMessage = `ZCZC TMQ2526 141605
|
||||
FF ZBTJZPZX
|
||||
141604 ZBACZQZX
|
||||
(ARR-JAE7433/A0132-RKSI-ZBTJ1604)
|
||||
NNNN`
|
||||
|
||||
benchDEPMessage = `ZCZC DEP5678 120915
|
||||
DD KLAXZPZX
|
||||
120914 KSFOZQZX
|
||||
(DEP-ABC5678-A1234-ZBTJ1440-ZGGG)
|
||||
NNNN`
|
||||
|
||||
benchCNLMessage = `ZCZC CNL9012 150631
|
||||
FF ZBTJZPZX
|
||||
(CNL-CCA9012-ZBTJ-ZGGG)
|
||||
NNNN`
|
||||
|
||||
benchDLAMessage = `ZCZC DLA3456 150631
|
||||
FF ZBTJZPZX
|
||||
(DLA-CCA3456-A1234-ZBTJ1600-ZGGG0200)
|
||||
NNNN`
|
||||
|
||||
benchFPLMessage = `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`
|
||||
|
||||
benchComplexFPLMessage = `ZCZC FPL7890 150631
|
||||
FF ZBTJZPZX
|
||||
(FPL-JAE7433-IS
|
||||
-B744/H-SXIRPZJWY/S
|
||||
-ZBTJ1755
|
||||
-K0926S0920 CG A326 VYK W80 HUR B339 GM A575 MANSA/K0919S0980
|
||||
-EDDF0948 EDDK
|
||||
-EET/ZMUB0100 UNKL0236
|
||||
REG/B2422 SEL/JLAD
|
||||
NAV/RNAV1 RNAV5 RNP4
|
||||
RMK/AGCS EQUIPPED)
|
||||
NNNN`
|
||||
)
|
||||
|
||||
// BenchmarkParseARR benchmarks parsing ARR messages
|
||||
func BenchmarkParseARR(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = Parse(benchARRMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseDEP benchmarks parsing DEP messages
|
||||
func BenchmarkParseDEP(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = Parse(benchDEPMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseCNL benchmarks parsing CNL messages
|
||||
func BenchmarkParseCNL(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = Parse(benchCNLMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseDLA benchmarks parsing DLA messages
|
||||
func BenchmarkParseDLA(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = Parse(benchDLAMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseFPL benchmarks parsing simple FPL messages
|
||||
func BenchmarkParseFPL(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = Parse(benchFPLMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseComplexFPL benchmarks parsing complex FPL messages with extensive route and metadata
|
||||
func BenchmarkParseComplexFPL(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = Parse(benchComplexFPLMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseHeader benchmarks header parsing only
|
||||
func BenchmarkParseHeader(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = ParseHeader(benchARRMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseBody benchmarks body parsing only (ARR)
|
||||
func BenchmarkParseBody(b *testing.B) {
|
||||
body := `(ARR-JAE7433/A0132-RKSI-ZBTJ1604)`
|
||||
parser := NewBodyParser(body)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _ = parser.Parse()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseMixed benchmarks parsing a mix of message types
|
||||
func BenchmarkParseMixed(b *testing.B) {
|
||||
messages := []string{
|
||||
benchARRMessage,
|
||||
benchDEPMessage,
|
||||
benchCNLMessage,
|
||||
benchDLAMessage,
|
||||
benchFPLMessage,
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
msg := messages[i%len(messages)]
|
||||
_, _ = Parse(msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
package aviation
|
||||
|
||||
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.Category).To(Equal("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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
package aviation
|
||||
|
||||
import "regexp"
|
||||
|
||||
// String constants
|
||||
const (
|
||||
StartIndicatorPrefix = "ZCZC"
|
||||
EndHeaderMarker = "."
|
||||
BeginPartMarker = "BEGIN PART"
|
||||
|
||||
Category = "category"
|
||||
CategoryArrival = "ARR"
|
||||
CategoryDeparture = "DEP"
|
||||
CategoryCancellation = "CNL"
|
||||
CategoryDelay = "DLA"
|
||||
CategoryFlightPlan = "FPL"
|
||||
|
||||
FlightNumber = "number"
|
||||
Register = "reg"
|
||||
|
||||
SSR = "ssr"
|
||||
DepartureCode = "dep"
|
||||
DepartureTime = "dep_time"
|
||||
ArrivalCode = "arr"
|
||||
ArrivalTime = "arr_time"
|
||||
DestinationCode = "dest"
|
||||
OtherInfo = "other"
|
||||
|
||||
ReferenceData = "reference_data"
|
||||
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"
|
||||
)
|
||||
|
||||
// Regular expression patterns
|
||||
const (
|
||||
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 (
|
||||
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}\/`)
|
||||
)
|
||||
@@ -0,0 +1,99 @@
|
||||
package aviation
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var (
|
||||
bodyPatterns = map[string]BodyConfig{}
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package aviation
|
||||
|
||||
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())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
package aviation
|
||||
|
||||
import "caatsm/internal/adapter/dto"
|
||||
|
||||
// AviationParser implements the Parser interface for aviation telegrams.
|
||||
type AviationParser struct{}
|
||||
|
||||
// NewParser creates a new aviation parser instance.
|
||||
func NewParser() *AviationParser {
|
||||
return &AviationParser{}
|
||||
}
|
||||
|
||||
// Parse parses a raw message string and returns a ParsedTelegram.
|
||||
func (p *AviationParser) Parse(rawText string) (*dto.ParsedTelegram, error) {
|
||||
return Parse(rawText)
|
||||
}
|
||||
Reference in New Issue
Block a user