refactor: Update AFTN message parsing logic
This commit is contained in:
@@ -1,81 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"caatsm/pkg/utils"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AFTN 定义AFTN报文的结构
|
||||
type AFTN struct {
|
||||
Header Header `json:"header"` // 报文头部信息
|
||||
PriorityAndSender PriorityAndSender `json:"priority_and_sender"` // 优先级和发送地址信息
|
||||
TimeAndReceiver TimeAndReceiver `json:"time_and_receiver"` // 时间和接收地址信息
|
||||
Body string `json:"body"` // 报文内容
|
||||
ReceivedTime time.Time `json:"received_time"` // 收报时间,表示电报接收到的时间
|
||||
Category string `json:"category"`
|
||||
BodyData interface{} `json:"body_data"`
|
||||
}
|
||||
|
||||
// Header 定义AFTN报文的报头
|
||||
type Header struct {
|
||||
StartSignal string `json:"start_signal"` // 启动信号,表示报文的开始,通常为固定值
|
||||
SendID string `json:"send_id"` // 发送编号,用于唯一标识报文
|
||||
SendTime string `json:"send_time"` // 发送时间,格式为DDHHMM
|
||||
}
|
||||
|
||||
// 示例:
|
||||
// Header{
|
||||
// StartSignal: "ZCZC",
|
||||
// SendID: "TMQ2611",
|
||||
// SendTime: "151524",
|
||||
// }
|
||||
|
||||
// PriorityAndSender 定义优先级和发送地址
|
||||
type PriorityAndSender struct {
|
||||
Priority string `json:"priority"` // 优先级
|
||||
Sender string `json:"sender"` // 发报地址
|
||||
}
|
||||
|
||||
// 示例:
|
||||
// PriorityAndSender{
|
||||
// Priority: "FF",
|
||||
// Sender: "ZBTJZPZX",
|
||||
// }
|
||||
|
||||
// TimeAndReceiver 定义时间和接收地址
|
||||
type TimeAndReceiver struct {
|
||||
Time string `json:"time"` // 时间
|
||||
Receiver string `json:"receiver"` // 收报地址
|
||||
}
|
||||
|
||||
// 示例:
|
||||
// TimeAndReceiver{
|
||||
// Time: "151524",
|
||||
// Receiver: "ZGGGZPZX",
|
||||
// }
|
||||
|
||||
// Origin 定义AFTN报文的来源
|
||||
type Origin struct {
|
||||
OriginCode string `json:"origin_code"` // 发报地址代码
|
||||
FiledTime time.Time `json:"filed_time"` // 签发时间,表示电报生成的时间
|
||||
}
|
||||
|
||||
func (h *Header) Validate() error {
|
||||
// Validate SendTime format (e.g., DDHHMM)
|
||||
if len(h.SendTime) != 6 {
|
||||
err := "invalid send_time format"
|
||||
|
||||
utils.Logger.Error(err)
|
||||
return fmt.Errorf(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AFTN) Validate() error {
|
||||
if err := a.Header.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add more validation as needed
|
||||
return nil
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("AFTN", func() {
|
||||
var original AFTN
|
||||
|
||||
BeforeEach(func() {
|
||||
original = AFTN{
|
||||
Header: Header{
|
||||
StartSignal: "ZCZC",
|
||||
SendID: "TMQ2611",
|
||||
SendTime: "151524",
|
||||
},
|
||||
PriorityAndSender: PriorityAndSender{
|
||||
Priority: "FF",
|
||||
Sender: "ZBTJZPZX",
|
||||
},
|
||||
TimeAndReceiver: TimeAndReceiver{
|
||||
Time: "151524",
|
||||
Receiver: "ZGGGZPZX",
|
||||
},
|
||||
Body: "Test message",
|
||||
ReceivedTime: time.Now(),
|
||||
Category: "Test",
|
||||
BodyData: nil,
|
||||
}
|
||||
})
|
||||
|
||||
Describe("Marshalling and Unmarshalling", func() {
|
||||
It("should marshal and unmarshal correctly", func() {
|
||||
data, err := json.Marshal(original)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var unmarshalled AFTN
|
||||
err = json.Unmarshal(data, &unmarshalled)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(unmarshalled.Header).To(Equal(original.Header))
|
||||
Expect(unmarshalled.PriorityAndSender).To(Equal(original.PriorityAndSender))
|
||||
Expect(unmarshalled.TimeAndReceiver).To(Equal(original.TimeAndReceiver))
|
||||
Expect(unmarshalled.Body).To(Equal(original.Body))
|
||||
Expect(unmarshalled.Category).To(Equal(original.Category))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
/*
|
||||
ZCZC TMQ1324 150631
|
||||
FF ZBTJZPZX
|
||||
@@ -24,5 +26,11 @@ type ParsedMessage struct {
|
||||
SecondaryAddresses []string // Additional recipient addresses
|
||||
Originator string // Sender of the message
|
||||
OriginatorDateTime string // Date and time when the originator sent the message
|
||||
Category string // Category of the message
|
||||
BodyAndFooter string
|
||||
BodyData interface{}
|
||||
ReceivedAt time.Time
|
||||
ParsedAt time.Time
|
||||
DispatchedAt time.Time
|
||||
NeedDispatch bool
|
||||
}
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"caatsm/internal/config"
|
||||
"caatsm/internal/domain"
|
||||
"caatsm/pkg/utils"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
textPattern = regexp.MustCompile(`\(([A-Z]{3})(.*)\)`)
|
||||
validPriority = regexp.MustCompile(`^(SS|DD|FF|GG|KK)$`)
|
||||
validAddress = regexp.MustCompile(`^[A-Z]{8}$`)
|
||||
emptyLineRemove = regexp.MustCompile(`(?m)^\s*$`)
|
||||
)
|
||||
|
||||
// AFTNParser is responsible for parsing AFTN messages.
|
||||
type AFTNParser struct {
|
||||
bodyPattern map[string]config.BodyConfig // Injected configuration for body patterns.
|
||||
}
|
||||
|
||||
// NewAFTNParser creates a new instance of AFTNParser.
|
||||
func NewAFTNParser(myPatterns map[string]config.BodyConfig) *AFTNParser {
|
||||
return &AFTNParser{bodyPattern: myPatterns}
|
||||
}
|
||||
|
||||
// DefaultParser creates a new instance of AFTNParser with default patterns.
|
||||
func DefaultParser() *AFTNParser {
|
||||
return &AFTNParser{bodyPattern: config.GetBodyPatterns()}
|
||||
}
|
||||
|
||||
func (p *AFTNParser) GetBodyPatterns() map[string]config.BodyConfig {
|
||||
return p.bodyPattern
|
||||
}
|
||||
|
||||
// Parse is the main method to parse an AFTN message.
|
||||
func (p *AFTNParser) Parse(rawMessage string) (*domain.AFTN, error) {
|
||||
cleanedText := removeEmptyLines(rawMessage)
|
||||
lines := strings.Split(cleanedText, "\n")
|
||||
|
||||
if len(lines) < 4 {
|
||||
return nil, fmt.Errorf("invalid AFTN message format: insufficient lines")
|
||||
}
|
||||
|
||||
header, err := parseHeader(lines[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priorityAndSender, err := parsePriorityAndSender(lines[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeAndReceiver, err := parseTimeAndReceiver(lines[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, category, err := extractBodyAndCategory(strings.Join(lines[3:], "\n"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bodyData, err := p.extractBodyData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aftn, err := p.createAFTN(bodyData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.AFTN{
|
||||
Header: header,
|
||||
PriorityAndSender: priorityAndSender,
|
||||
TimeAndReceiver: timeAndReceiver,
|
||||
Body: body,
|
||||
Category: category,
|
||||
BodyData: aftn,
|
||||
ReceivedTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseHeader parses the header line of an AFTN message.
|
||||
func parseHeader(line string) (domain.Header, error) {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 3 {
|
||||
return domain.Header{}, fmt.Errorf("invalid header format: %s", line)
|
||||
}
|
||||
return domain.Header{
|
||||
StartSignal: parts[0],
|
||||
SendID: parts[1],
|
||||
SendTime: parts[2],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parsePriorityAndSender parses the priority and sender line of an AFTN message.
|
||||
func parsePriorityAndSender(line string) (domain.PriorityAndSender, error) {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
return domain.PriorityAndSender{}, fmt.Errorf("invalid priority and sender format: %s", line)
|
||||
}
|
||||
return domain.PriorityAndSender{
|
||||
Priority: parts[0],
|
||||
Sender: parts[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseTimeAndReceiver parses the time and receiver line of an AFTN message.
|
||||
func parseTimeAndReceiver(line string) (domain.TimeAndReceiver, error) {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
return domain.TimeAndReceiver{}, fmt.Errorf("invalid time and receiver format: %s", line)
|
||||
}
|
||||
return domain.TimeAndReceiver{
|
||||
Time: parts[0],
|
||||
Receiver: parts[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseTextInfo parses the text and extracts the body type from an AFTN message.
|
||||
func extractBodyAndCategory(text string) (string, string, error) {
|
||||
match := textPattern.FindStringSubmatch(text)
|
||||
if len(match) > 1 {
|
||||
return match[0], match[1], nil
|
||||
}
|
||||
return "", "", fmt.Errorf("invalid text format: %s", text)
|
||||
}
|
||||
|
||||
func (p *AFTNParser) ParseBody(body string) (interface{}, error) {
|
||||
bodyData, err := p.extractBodyData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aftn, err := p.createAFTN(bodyData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aftn, nil
|
||||
}
|
||||
|
||||
// parseBody parses the body data of an AFTN message.
|
||||
func (p *AFTNParser) createAFTN(data map[string]string) (interface{}, error) {
|
||||
switch data["type"] {
|
||||
case "ARR":
|
||||
return &domain.ARR{
|
||||
Category: data["type"],
|
||||
AircraftID: data["number"],
|
||||
SSRModeAndCode: data["ssr"],
|
||||
DepartureAirport: data["departure"],
|
||||
ArrivalAirport: data["arrival"],
|
||||
}, nil
|
||||
case "DEP":
|
||||
return &domain.DEP{
|
||||
Category: data["type"],
|
||||
AircraftID: data["number"],
|
||||
SSRModeAndCode: data["ssr"],
|
||||
DepartureAirport: data["departure"],
|
||||
DepartureTime: data["departure_time"],
|
||||
Destination: data["arrival"],
|
||||
}, nil
|
||||
case "FPL":
|
||||
return &domain.FPL{
|
||||
Category: data["type"],
|
||||
FlightNumber: data["number"],
|
||||
ReferenceData: data["reference_data"],
|
||||
AircraftID: data["aircraft"],
|
||||
SSRModeAndCode: data["surve"],
|
||||
FlightRulesAndType: data["indicator"],
|
||||
CruisingSpeedAndLevel: data["speed"] + data["level"],
|
||||
DepartureAirport: data["departure"],
|
||||
DepartureTime: data["departure_time"],
|
||||
Route: data["route"],
|
||||
DestinationAndTotalTime: data["destination"] + data["estt"],
|
||||
AlternateAirport: data["alter"],
|
||||
OtherInfo: fmt.Sprintf("%s %s REG/%s EET/%s SEL/%s PER/%s RIF/%s",
|
||||
data["pbn"], data["nav"], data["reg"], data["eet"], data["sel"], data["performance"], data["rif"]),
|
||||
SupplementaryInfo: "RMK/" + data["remark"],
|
||||
EstimatedArrivalTime: data["estimated_arrival_time"],
|
||||
PBN: data["pbn"],
|
||||
NavigationEquipment: data["nav"],
|
||||
EstimatedElapsedTime: data["eet"],
|
||||
SELCALCode: data["sel"],
|
||||
PerformanceCategory: data["performance"],
|
||||
RerouteInformation: data["rif"],
|
||||
Remarks: data["remark"],
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid message type: %s", data["type"])
|
||||
}
|
||||
}
|
||||
|
||||
// extractBodyData extracts the body data from an AFTN message.
|
||||
func (p *AFTNParser) extractBodyData(text string) (map[string]string, error) {
|
||||
log := utils.Logger
|
||||
data := make(map[string]string)
|
||||
for _, pattern := range p.GetBodyPatterns() {
|
||||
for i, p := range pattern.Patterns {
|
||||
log.Debugf("Trying pattern %d: %s\n %s\n", i, p.Comments, p.Pattern)
|
||||
re := p.Expression
|
||||
match := re.FindStringSubmatch(text)
|
||||
if match != nil {
|
||||
log.Debugf("Matched : %v \n", match)
|
||||
for i, name := range re.SubexpNames() {
|
||||
if i != 0 && name != "" {
|
||||
data[name] = match[i]
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
log.Debugf("No match for pattern %d\n", i)
|
||||
}
|
||||
}
|
||||
log.Errorf("No matching pattern found for text: %s", text)
|
||||
return nil, fmt.Errorf("no matching pattern found for text: %s", text)
|
||||
}
|
||||
|
||||
// removeEmptyLines removes empty lines from a given text.
|
||||
func removeEmptyLines(text string) string {
|
||||
return emptyLineRemove.ReplaceAllString(text, "")
|
||||
}
|
||||
|
||||
// ValidateAFTN validates the fields of an AFTN message.
|
||||
func ValidateAFTN(msg *domain.AFTN) error {
|
||||
if missingRequiredFields(msg) {
|
||||
return fmt.Errorf("invalid AFTN message: missing fields")
|
||||
}
|
||||
|
||||
if !validPriority.MatchString(msg.PriorityAndSender.Priority) {
|
||||
return fmt.Errorf("invalid priority code: %s", msg.PriorityAndSender.Priority)
|
||||
}
|
||||
|
||||
if !validAddress.MatchString(msg.TimeAndReceiver.Receiver) || !validAddress.MatchString(msg.PriorityAndSender.Sender) {
|
||||
return fmt.Errorf("invalid address format")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// missingRequiredFields checks if required fields in an AFTN message are missing.
|
||||
func missingRequiredFields(msg *domain.AFTN) bool {
|
||||
return msg.PriorityAndSender.Priority == "" ||
|
||||
msg.TimeAndReceiver.Receiver == "" ||
|
||||
msg.PriorityAndSender.Sender == "" ||
|
||||
msg.Header.StartSignal == "" ||
|
||||
msg.Header.SendID == "" ||
|
||||
msg.Header.SendTime == ""
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"caatsm/internal/domain"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("AFTN Parser", func() {
|
||||
Describe("Parse", func() {
|
||||
var parser *AFTNParser
|
||||
BeforeEach(func() {
|
||||
parser = DefaultParser()
|
||||
})
|
||||
|
||||
It("should parse ARR messages correctly", func() {
|
||||
message := "(ARR-AB123-SSR1234-KJFK-KLAX)"
|
||||
// parser := DefaultParser()
|
||||
parsedMessage, err := parser.ParseBody(message)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(parsedMessage).To(BeAssignableToTypeOf(&domain.ARR{}))
|
||||
arrMessage := parsedMessage.(*domain.ARR)
|
||||
|
||||
Expect(arrMessage.Category).To(Equal("ARR"))
|
||||
Expect(arrMessage.AircraftID).To(Equal("AB123"))
|
||||
Expect(arrMessage.SSRModeAndCode).To(Equal("SSR1234"))
|
||||
Expect(arrMessage.DepartureAirport).To(Equal("KJFK"))
|
||||
Expect(arrMessage.ArrivalAirport).To(Equal("KLAX"))
|
||||
})
|
||||
|
||||
It("should parse DEP messages correctly", func() {
|
||||
message := "(DEP-AB123-SSR1234-KJFK-1500-KLAX)"
|
||||
parsedMessage, err := parser.ParseBody(message)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(parsedMessage).To(BeAssignableToTypeOf(&domain.DEP{}))
|
||||
depMessage := parsedMessage.(*domain.DEP)
|
||||
Expect(depMessage.Category).To(Equal("DEP"))
|
||||
Expect(depMessage.AircraftID).To(Equal("AB123"))
|
||||
Expect(depMessage.SSRModeAndCode).To(Equal("SSR1234"))
|
||||
Expect(depMessage.DepartureAirport).To(Equal("KJFK"))
|
||||
Expect(depMessage.DepartureTime).To(Equal("1500"))
|
||||
Expect(depMessage.Destination).To(Equal("KLAX"))
|
||||
})
|
||||
|
||||
It("should parse FPL messages correctly", func() {
|
||||
message := `(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)`
|
||||
parsedMessage, err := parser.ParseBody(message)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(parsedMessage).To(BeAssignableToTypeOf(&domain.FPL{}))
|
||||
fplMessage := parsedMessage.(*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"))
|
||||
// fmt.Println(fplMessage.OtherInfo)
|
||||
Expect(fplMessage.OtherInfo).To(Equal("PBN/A1B2B3B4B5D1L1 NAV/ABAS REG/B6513 EET/ZBPE0112 SEL/KMAL PER/C RIF/FRT N640 ZBYN"))
|
||||
Expect(fplMessage.SupplementaryInfo).To(Equal("RMK/TCAS EQUIPPED"))
|
||||
Expect(fplMessage.PBN).To(Equal("PBN/A1B2B3B4B5D1L1"))
|
||||
// fmt.Println(fplMessage.EstimatedElapsedTime)
|
||||
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"))
|
||||
})
|
||||
|
||||
It("should return an error for invalid message types", func() {
|
||||
message := "(XYZ-AB123-SSR1234-KJFK-KLAX)"
|
||||
_, err := parser.ParseBody(message)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(Equal("invalid message type: XYZ"))
|
||||
})
|
||||
|
||||
It("should parse a valid AFTN message", func() {
|
||||
rawMessage := `ZCZC TMQ2611 151524
|
||||
FF SENDERAA
|
||||
151524 RECEIVERAA
|
||||
(ARR-AB123-SSR1234-KJFK-KLAX)`
|
||||
|
||||
aftnMessage, err := parser.Parse(rawMessage)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(aftnMessage).NotTo(BeNil())
|
||||
Expect(aftnMessage.Header.StartSignal).To(Equal("ZCZC"))
|
||||
Expect(aftnMessage.Header.SendID).To(Equal("TMQ2611"))
|
||||
Expect(aftnMessage.Header.SendTime).To(Equal("151524"))
|
||||
Expect(aftnMessage.Category).To(Equal("ARR"))
|
||||
})
|
||||
|
||||
It("should return an error for invalid AFTN message format", func() {
|
||||
rawMessage := `ZCZC
|
||||
TMQ2611
|
||||
151524`
|
||||
|
||||
_, err := parser.Parse(rawMessage)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(Equal("invalid AFTN message format: insufficient lines"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ValidateAFTN", func() {
|
||||
It("should validate a valid AFTN message", func() {
|
||||
aftnMessage := &domain.AFTN{
|
||||
Header: domain.Header{
|
||||
StartSignal: "ZCZC",
|
||||
SendID: "TMQ2611",
|
||||
SendTime: "151524",
|
||||
},
|
||||
PriorityAndSender: domain.PriorityAndSender{
|
||||
Priority: "FF",
|
||||
Sender: "SENDERAA",
|
||||
},
|
||||
TimeAndReceiver: domain.TimeAndReceiver{
|
||||
Time: "151524",
|
||||
Receiver: "RECEIVAA",
|
||||
},
|
||||
Category: "ARR",
|
||||
}
|
||||
err := ValidateAFTN(aftnMessage)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should return an error for missing required fields", func() {
|
||||
aftnMessage := &domain.AFTN{
|
||||
Header: domain.Header{
|
||||
StartSignal: "",
|
||||
SendID: "TMQ2611",
|
||||
SendTime: "151524",
|
||||
},
|
||||
PriorityAndSender: domain.PriorityAndSender{
|
||||
Priority: "FF",
|
||||
Sender: "SENDERAA",
|
||||
},
|
||||
TimeAndReceiver: domain.TimeAndReceiver{
|
||||
Time: "151524",
|
||||
Receiver: "RECEIVAA",
|
||||
},
|
||||
Category: "ARR",
|
||||
}
|
||||
err := ValidateAFTN(aftnMessage)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(Equal("invalid AFTN message: missing fields"))
|
||||
})
|
||||
|
||||
It("should return an error for invalid priority code", func() {
|
||||
aftnMessage := &domain.AFTN{
|
||||
Header: domain.Header{
|
||||
StartSignal: "ZCZC",
|
||||
SendID: "TMQ2611",
|
||||
SendTime: "151524",
|
||||
},
|
||||
PriorityAndSender: domain.PriorityAndSender{
|
||||
Priority: "ZZ",
|
||||
Sender: "SENDERAA",
|
||||
},
|
||||
TimeAndReceiver: domain.TimeAndReceiver{
|
||||
Time: "151524",
|
||||
Receiver: "RECEIVAA",
|
||||
},
|
||||
Category: "ARR",
|
||||
}
|
||||
err := ValidateAFTN(aftnMessage)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(Equal("invalid priority code: ZZ"))
|
||||
})
|
||||
|
||||
It("should return an error for invalid address format", func() {
|
||||
aftnMessage := &domain.AFTN{
|
||||
Header: domain.Header{
|
||||
StartSignal: "ZCZC",
|
||||
SendID: "TMQ2611",
|
||||
SendTime: "151524",
|
||||
},
|
||||
PriorityAndSender: domain.PriorityAndSender{
|
||||
Priority: "FF",
|
||||
Sender: "INVALID",
|
||||
},
|
||||
TimeAndReceiver: domain.TimeAndReceiver{
|
||||
Time: "151524",
|
||||
Receiver: "INVALID",
|
||||
},
|
||||
Category: "ARR",
|
||||
}
|
||||
err := ValidateAFTN(aftnMessage)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(Equal("invalid address format"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,11 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"caatsm/internal/config"
|
||||
"caatsm/internal/domain"
|
||||
"caatsm/pkg/utils"
|
||||
"errors" // Import errors package to handle errors
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -12,6 +15,110 @@ const (
|
||||
BeginPartMarker = "BEGIN PART"
|
||||
)
|
||||
|
||||
type BodyParser struct {
|
||||
bodyPattern map[string]config.BodyConfig // Injected configuration for body patterns.
|
||||
}
|
||||
|
||||
func DefaultBodyParser() *BodyParser {
|
||||
return &BodyParser{bodyPattern: config.GetBodyPatterns()}
|
||||
}
|
||||
|
||||
func (bp *BodyParser) GetBodyPatterns() map[string]config.BodyConfig {
|
||||
return bp.bodyPattern
|
||||
}
|
||||
|
||||
func (bp *BodyParser) SetBodyPatterns(patterns map[string]config.BodyConfig) {
|
||||
bp.bodyPattern = patterns
|
||||
}
|
||||
|
||||
func (bp *BodyParser) Parse(body string) (interface{}, error) {
|
||||
log := utils.Logger
|
||||
data := make(map[string]string)
|
||||
for _, pattern := range bp.GetBodyPatterns() {
|
||||
for i, p := range pattern.Patterns {
|
||||
log.Debugf("Trying pattern %d: %s\n %s\n", i, p.Comments, p.Pattern)
|
||||
re := p.Expression
|
||||
match := re.FindStringSubmatch(body)
|
||||
if match != nil {
|
||||
log.Debugf("Matched : %v \n", match)
|
||||
for i, name := range re.SubexpNames() {
|
||||
if i != 0 && name != "" {
|
||||
data[name] = match[i]
|
||||
}
|
||||
}
|
||||
return CreateBodyData(data)
|
||||
}
|
||||
log.Debugf("No match for pattern %d\n", i)
|
||||
}
|
||||
}
|
||||
log.Errorf("No matching pattern found for body: %s", body)
|
||||
return data, errors.New("no matching pattern found for body")
|
||||
}
|
||||
|
||||
func CreateBodyData(data map[string]string) (interface{}, error) {
|
||||
switch data["type"] {
|
||||
case "ARR":
|
||||
return &domain.ARR{
|
||||
Category: data["type"],
|
||||
AircraftID: data["number"],
|
||||
SSRModeAndCode: data["ssr"],
|
||||
DepartureAirport: data["departure"],
|
||||
ArrivalAirport: data["arrival"],
|
||||
}, nil
|
||||
case "DEP":
|
||||
return &domain.DEP{
|
||||
Category: data["type"],
|
||||
AircraftID: data["number"],
|
||||
SSRModeAndCode: data["ssr"],
|
||||
DepartureAirport: data["departure"],
|
||||
DepartureTime: data["departure_time"],
|
||||
Destination: data["arrival"],
|
||||
}, nil
|
||||
case "FPL":
|
||||
return &domain.FPL{
|
||||
Category: data["type"],
|
||||
FlightNumber: data["number"],
|
||||
ReferenceData: data["reference_data"],
|
||||
AircraftID: data["aircraft"],
|
||||
SSRModeAndCode: data["surve"],
|
||||
FlightRulesAndType: data["indicator"],
|
||||
CruisingSpeedAndLevel: data["speed"] + data["level"],
|
||||
DepartureAirport: data["departure"],
|
||||
DepartureTime: data["departure_time"],
|
||||
Route: data["route"],
|
||||
DestinationAndTotalTime: data["destination"] + data["estt"],
|
||||
AlternateAirport: data["alter"],
|
||||
OtherInfo: fmt.Sprintf("%s %s REG/%s EET/%s SEL/%s PER/%s RIF/%s",
|
||||
data["pbn"], data["nav"], data["reg"], data["eet"], data["sel"], data["performance"], data["rif"]),
|
||||
SupplementaryInfo: "RMK/" + data["remark"],
|
||||
EstimatedArrivalTime: data["estimated_arrival_time"],
|
||||
PBN: data["pbn"],
|
||||
NavigationEquipment: data["nav"],
|
||||
EstimatedElapsedTime: data["eet"],
|
||||
SELCALCode: data["sel"],
|
||||
PerformanceCategory: data["performance"],
|
||||
RerouteInformation: data["rif"],
|
||||
Remarks: data["remark"],
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid message type: %s", data["type"])
|
||||
}
|
||||
}
|
||||
|
||||
func Parse(rawText string) (*domain.ParsedMessage, error) {
|
||||
message, err := ParseHeader(rawText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyParser := DefaultBodyParser()
|
||||
bodyData, err := bodyParser.Parse(message.BodyAndFooter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message.BodyData = bodyData
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// ParseHeader parses the header of the message and returns a ParsedMessage struct
|
||||
func ParseHeader(fullMessage string) (domain.ParsedMessage, error) {
|
||||
fullMessage = strings.TrimSpace(fullMessage) // Trim leading and trailing spaces
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"caatsm/internal/domain"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@@ -121,4 +123,77 @@ NNNN
|
||||
`))
|
||||
})
|
||||
})
|
||||
Describe("ParseBody", func() {
|
||||
Context("with ARR body", func() {
|
||||
parser := DefaultBodyParser()
|
||||
It("should parse the body (ARR-AB123-SSR1234-KJFK-KLAX) correctly", func() {
|
||||
body := "(ARR-AB123-SSR1234-KJFK-KLAX)"
|
||||
parsedBody, err := parser.Parse(body)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(parsedBody).ToNot(BeNil())
|
||||
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("SSR1234"))
|
||||
Expect(arrMessage.DepartureAirport).To(Equal("KJFK"))
|
||||
Expect(arrMessage.ArrivalAirport).To(Equal("KLAX"))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Context("with DEP body", func() {
|
||||
parser := DefaultBodyParser()
|
||||
It("should parse the body (DEP-AB123-SSR1234-KJFK-1500-KLAX) correctly", func() {
|
||||
body := "(DEP-AB123-SSR1234-KJFK-1500-KLAX)"
|
||||
parsedBody, err := parser.Parse(body)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(parsedBody).ToNot(BeNil())
|
||||
Expect(parsedBody).To(BeAssignableToTypeOf(&domain.DEP{}))
|
||||
depMessage := parsedBody.(*domain.DEP)
|
||||
Expect(depMessage.Category).To(Equal("DEP"))
|
||||
Expect(depMessage.AircraftID).To(Equal("AB123"))
|
||||
Expect(depMessage.SSRModeAndCode).To(Equal("SSR1234"))
|
||||
Expect(depMessage.DepartureAirport).To(Equal("KJFK"))
|
||||
Expect(depMessage.DepartureTime).To(Equal("1500"))
|
||||
Expect(depMessage.Destination).To(Equal("KLAX"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with FPL body", func() {
|
||||
parser := DefaultBodyParser()
|
||||
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)`
|
||||
parsedBody, err := parser.Parse(body)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(parsedBody).ToNot(BeNil())
|
||||
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"))
|
||||
Expect(fplMessage.SupplementaryInfo).To(Equal("RMK/TCAS EQUIPPED"))
|
||||
Expect(fplMessage.PBN).To(Equal("PBN/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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"caatsm/internal/config"
|
||||
"caatsm/internal/domain"
|
||||
"caatsm/pkg/utils"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
sitaTextPattern = regexp.MustCompile(`\(([A-Z]{3})(.*)\)`)
|
||||
validSitaPriority = regexp.MustCompile(`^(SS|DD|FF|GG|KK)$`)
|
||||
validSitaAddress = regexp.MustCompile(`^[A-Z]{8}$`)
|
||||
)
|
||||
|
||||
// SITAParser is responsible for parsing SITA messages.
|
||||
type SITAParser struct {
|
||||
bodyPattern map[string]config.BodyConfig // Injected configuration for body patterns.
|
||||
}
|
||||
|
||||
// NewSITAParser creates a new instance of SITAParser.
|
||||
func NewSITAParser(myPatterns map[string]config.BodyConfig) *SITAParser {
|
||||
return &SITAParser{bodyPattern: myPatterns}
|
||||
}
|
||||
|
||||
// DefaultSITAParser creates a new instance of SITAParser with default patterns.
|
||||
func DefaultSITAParser() *SITAParser {
|
||||
return &SITAParser{bodyPattern: config.GetBodyPatterns()}
|
||||
}
|
||||
|
||||
func (p *SITAParser) GetBodyPatterns() map[string]config.BodyConfig {
|
||||
return p.bodyPattern
|
||||
}
|
||||
|
||||
// Parse is the main method to parse a SITA message.
|
||||
func (p *SITAParser) Parse(rawMessage string) (*domain.SITA, error) {
|
||||
cleanedText := removeEmptyLines(rawMessage)
|
||||
lines := strings.Split(cleanedText, "\n")
|
||||
|
||||
if len(lines) < 4 {
|
||||
return nil, fmt.Errorf("invalid SITA message format: insufficient lines")
|
||||
}
|
||||
|
||||
header, err := parseSITAHeader(lines[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priorityAndSender, err := parseSITAPriorityAndSender(lines[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeAndReceiver, err := parseSITATimeAndReceiver(lines[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
text, bodyType, err := parseSITATextInfo(strings.Join(lines[3:], "\n"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bodyData, err := p.extractBodyData(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sita, err := p.createSITA(bodyData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.SITA{
|
||||
Header: header,
|
||||
PriorityAndSender: priorityAndSender,
|
||||
TimeAndReceiver: timeAndReceiver,
|
||||
Text: text,
|
||||
Category: bodyType,
|
||||
BodyData: sita,
|
||||
ReceivedTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseSITAHeader parses the header line of a SITA message.
|
||||
func parseSITAHeader(line string) (domain.SITAHeader, error) {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 3 {
|
||||
return domain.SITAHeader{}, fmt.Errorf("invalid header format: %s", line)
|
||||
}
|
||||
return domain.SITAHeader{
|
||||
StartSignal: parts[0],
|
||||
SendID: parts[1],
|
||||
SendTime: parts[2],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseSITAPriorityAndSender parses the priority and sender line of a SITA message.
|
||||
func parseSITAPriorityAndSender(line string) (domain.PrioritySender, error) {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
return domain.PrioritySender{}, fmt.Errorf("invalid priority and sender format: %s", line)
|
||||
}
|
||||
return domain.PrioritySender{
|
||||
Priority: parts[0],
|
||||
Sender: parts[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseSITATimeAndReceiver parses the time and receiver line of a SITA message.
|
||||
func parseSITATimeAndReceiver(line string) (domain.TimeReceiver, error) {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
return domain.TimeReceiver{}, fmt.Errorf("invalid time and receiver format: %s", line)
|
||||
}
|
||||
return domain.TimeReceiver{
|
||||
Time: parts[0],
|
||||
Receiver: parts[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseSITATextInfo parses the text and extracts the body type from a SITA message.
|
||||
func parseSITATextInfo(text string) (string, string, error) {
|
||||
match := sitaTextPattern.FindStringSubmatch(text)
|
||||
if len(match) > 1 {
|
||||
return match[0], match[1], nil
|
||||
}
|
||||
return "", "", fmt.Errorf("invalid text format: %s", text)
|
||||
}
|
||||
|
||||
func (p *SITAParser) ParseBody(body string) (interface{}, error) {
|
||||
bodyData, err := p.extractBodyData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sita, err := p.createSITA(bodyData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sita, nil
|
||||
}
|
||||
|
||||
// createSITA parses the body data of a SITA message.
|
||||
func (p *SITAParser) createSITA(data map[string]string) (interface{}, error) {
|
||||
switch data["type"] {
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid message type: %s", data["type"])
|
||||
}
|
||||
}
|
||||
|
||||
// extractBodyData extracts the body data from a SITA message.
|
||||
func (p *SITAParser) extractBodyData(text string) (map[string]string, error) {
|
||||
log := utils.Logger
|
||||
data := make(map[string]string)
|
||||
for _, pattern := range p.GetBodyPatterns() {
|
||||
for i, p := range pattern.Patterns {
|
||||
log.Debugf("Trying pattern %d: %s\n %s\n", i, p.Comments, p.Pattern)
|
||||
re := p.Expression
|
||||
match := re.FindStringSubmatch(text)
|
||||
if match != nil {
|
||||
log.Debugf("Matched : %v \n", match)
|
||||
for i, name := range re.SubexpNames() {
|
||||
if i != 0 && name != "" {
|
||||
data[name] = match[i]
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
log.Debugf("No match for pattern %d\n", i)
|
||||
}
|
||||
}
|
||||
log.Errorf("No matching pattern found for text: %s", text)
|
||||
return nil, fmt.Errorf("no matching pattern found for text: %s", text)
|
||||
}
|
||||
|
||||
// ValidateSITA validates the fields of a SITA message.
|
||||
func ValidateSITA(msg *domain.SITA) error {
|
||||
if missingSitaFields(msg) {
|
||||
return fmt.Errorf("invalid SITA message: missing fields")
|
||||
}
|
||||
|
||||
if !validSitaPriority.MatchString(msg.PriorityAndSender.Priority) {
|
||||
return fmt.Errorf("invalid priority code: %s", msg.PriorityAndSender.Priority)
|
||||
}
|
||||
|
||||
if !validSitaAddress.MatchString(msg.TimeAndReceiver.Receiver) || !validSitaAddress.MatchString(msg.PriorityAndSender.Sender) {
|
||||
return fmt.Errorf("invalid address format")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func missingSitaFields(msg *domain.SITA) bool {
|
||||
if msg.Header.StartSignal == "" || msg.Header.SendID == "" || msg.Header.SendTime == "" ||
|
||||
msg.PriorityAndSender.Priority == "" || msg.PriorityAndSender.Sender == "" ||
|
||||
msg.TimeAndReceiver.Time == "" || msg.TimeAndReceiver.Receiver == "" ||
|
||||
msg.Category == "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user