From 2b83d9b78518d57f3c39d1f0a473344ba0bdd667 Mon Sep 17 00:00:00 2001 From: windyboy Date: Sat, 20 Jul 2024 22:21:12 +0800 Subject: [PATCH] refactor: Update AFTN message parsing logic --- internal/domain/aftn.go | 81 ------- internal/domain/aftn_test.go | 52 ----- internal/domain/aviation.go | 8 + internal/parsers/aftn_parser.go | 255 ----------------------- internal/parsers/aftn_parser_test.go | 201 ------------------ internal/parsers/aviation_parser.go | 107 ++++++++++ internal/parsers/aviation_parser_test.go | 75 +++++++ internal/parsers/sita_parser.go | 207 ------------------ 8 files changed, 190 insertions(+), 796 deletions(-) delete mode 100644 internal/domain/aftn.go delete mode 100644 internal/domain/aftn_test.go delete mode 100644 internal/parsers/aftn_parser.go delete mode 100644 internal/parsers/aftn_parser_test.go delete mode 100644 internal/parsers/sita_parser.go diff --git a/internal/domain/aftn.go b/internal/domain/aftn.go deleted file mode 100644 index 9734ffe..0000000 --- a/internal/domain/aftn.go +++ /dev/null @@ -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 -} diff --git a/internal/domain/aftn_test.go b/internal/domain/aftn_test.go deleted file mode 100644 index 596ac2e..0000000 --- a/internal/domain/aftn_test.go +++ /dev/null @@ -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)) - }) - }) -}) diff --git a/internal/domain/aviation.go b/internal/domain/aviation.go index 02cbda2..4e4491f 100644 --- a/internal/domain/aviation.go +++ b/internal/domain/aviation.go @@ -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 } diff --git a/internal/parsers/aftn_parser.go b/internal/parsers/aftn_parser.go deleted file mode 100644 index 6a4e7d1..0000000 --- a/internal/parsers/aftn_parser.go +++ /dev/null @@ -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 == "" -} diff --git a/internal/parsers/aftn_parser_test.go b/internal/parsers/aftn_parser_test.go deleted file mode 100644 index 399c4d8..0000000 --- a/internal/parsers/aftn_parser_test.go +++ /dev/null @@ -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")) - }) - }) -}) diff --git a/internal/parsers/aviation_parser.go b/internal/parsers/aviation_parser.go index e31ef5b..da52e8c 100644 --- a/internal/parsers/aviation_parser.go +++ b/internal/parsers/aviation_parser.go @@ -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 diff --git a/internal/parsers/aviation_parser_test.go b/internal/parsers/aviation_parser_test.go index 34f03b3..821f90e 100644 --- a/internal/parsers/aviation_parser_test.go +++ b/internal/parsers/aviation_parser_test.go @@ -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")) + }) + }) + }) }) diff --git a/internal/parsers/sita_parser.go b/internal/parsers/sita_parser.go deleted file mode 100644 index 73a5c8e..0000000 --- a/internal/parsers/sita_parser.go +++ /dev/null @@ -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 -}