This commit is contained in:
windyboy
2024-07-19 20:02:18 +08:00
parent d1d427057e
commit dbbf6133be
26 changed files with 2171 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
package parsers
import (
"caatsm/internal/config"
"caatsm/internal/domain"
"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*$`)
)
type AFTNParser struct {
BodyPattern *config.BodyConfig
}
// Parse parses a generic AFTN message based on its type.
func (p AFTNParser) Parse(text string) (interface{}, error) {
config := config.GetMyConfig()
data := ParseBody(text, config)
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{
AircraftID: data["number"],
SSRModeAndCode: data["ssr"],
DepartureAirport: data["departure"],
DepartureTime: data["departure_time"],
Destination: data["arrival"],
}, nil
default:
return nil, fmt.Errorf("invalid message type")
}
}
// removeEmptyLines removes empty lines from a given text.
func removeEmptyLines(text string) string {
cleanedText := emptyLineRemove.ReplaceAllString(text, "")
return strings.ReplaceAll(cleanedText, "\n\n", "\n")
}
// ParseAFTN parses an AFTN message from raw text.
func ParseAFTN(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")
}
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
}
text, bodyType, err := parseText(strings.Join(lines[3:], "\n"))
if err != nil {
return nil, err
}
bodyData, err := parseBodyData(text)
if err != nil {
return nil, err
}
return &domain.AFTN{
Header: header,
PriorityAndSender: priorityAndSender,
TimeAndReceiver: timeAndReceiver,
Text: text,
Category: bodyType,
BodyData: bodyData,
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")
}
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")
}
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")
}
return domain.TimeAndReceiver{
Time: parts[0],
Receiver: parts[1],
}, nil
}
// parseText parses the text and extracts the body type from an AFTN message.
func parseText(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")
}
// parseBodyData parses the body data of an AFTN message.
func parseBodyData(text string) (interface{}, error) {
bodyParser := AFTNParser{}
bodyData, err := bodyParser.Parse(text)
if err != nil {
return nil, fmt.Errorf("failed to parse body data: %w", err)
}
return bodyData, nil
}
// 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")
}
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 == ""
}
+194
View File
@@ -0,0 +1,194 @@
package parsers
import (
"caatsm/internal/config"
"caatsm/internal/domain"
"regexp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("AFTN Parser", func() {
var testConfig *config.Config
BeforeEach(func() {
testConfig = &config.Config{
Body: []config.BodyConfig{
{
Name: "ARR",
Patterns: []config.PatternConfig{
{
Pattern: `^\((?P<type>[A-Z]{3})\-(?P<number>[A-Z0-9]+)\-(?P<ssr>[A-Z0-9]+)\-(?P<departure>[A-Z]{4})\-(?P<arrival>[A-Z]{4})\)$`,
Expression: regexp.MustCompile(`^\((?P<type>[A-Z]{3})\-(?P<number>[A-Z0-9]+)\-(?P<ssr>[A-Z0-9]+)\-(?P<departure>[A-Z]{4})\-(?P<arrival>[A-Z]{4})\)$`),
},
},
},
{
Name: "DEP",
Patterns: []config.PatternConfig{
{
Pattern: `^\((?P<type>[A-Z]{3})\-(?P<number>[A-Z0-9]+)\-(?P<ssr>[A-Z0-9]+)\-(?P<departure>[A-Z]{4})\-(?P<departure_time>\d{4})\-(?P<arrival>[A-Z]{4})\)$`,
Expression: regexp.MustCompile(`^\((?P<type>[A-Z]{3})\-(?P<number>[A-Z0-9]+)\-(?P<ssr>[A-Z0-9]+)\-(?P<departure>[A-Z]{4})\-(?P<departure_time>\d{4})\-(?P<arrival>[A-Z]{4})\)$`),
},
},
},
},
}
config.SetMyConfig(testConfig)
})
Describe("Parse", func() {
It("should parse ARR messages correctly", func() {
message := "(ARR-AB123-SSR1234-KJFK-KLAX)"
parser := AFTNParser{}
parsedMessage, err := parser.Parse(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)"
parser := AFTNParser{}
parsedMessage, err := parser.Parse(message)
Expect(err).NotTo(HaveOccurred())
Expect(parsedMessage).To(BeAssignableToTypeOf(&domain.DEP{}))
depMessage := parsedMessage.(*domain.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 return an error for invalid message types", func() {
message := "(XYZ-AB123-SSR1234-KJFK-KLAX)"
parser := AFTNParser{}
_, err := parser.Parse(message)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("invalid message type"))
})
})
Describe("ParseAFTN", func() {
It("should parse a valid AFTN message", func() {
rawMessage := `ZCZC TMQ2611 151524
FF SENDERAA
151524 RECEIVER
(ARR-AB123-SSR1234-KJFK-KLAX)`
aftnMessage, err := ParseAFTN(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 := ParseAFTN(rawMessage)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("invalid AFTN message format"))
})
})
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: "RECEIVER",
},
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: "",
},
TimeAndReceiver: domain.TimeAndReceiver{
Time: "151524",
Receiver: "RECEIVER",
},
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: "RECEIVER",
},
Category: "ARR",
}
err := ValidateAFTN(aftnMessage)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("invalid priority code"))
})
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: "RECEIVERAA",
},
Category: "ARR",
}
err := ValidateAFTN(aftnMessage)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("invalid address format"))
})
})
})
+39
View File
@@ -0,0 +1,39 @@
package parsers
import (
"caatsm/internal/config"
"regexp"
)
var bodyTypePattern = regexp.MustCompile(`^\(([A-Z]{3})(.*\n?)+\)$`)
// FindPatterns detects the message body type based on the configuration.
func FindPatterns(messageBody string, config *config.Config) *config.BodyConfig {
if match := bodyTypePattern.FindStringSubmatch(messageBody); len(match) > 1 {
name := match[1]
for _, body := range config.Body {
if body.Name == name {
return &body
}
}
}
return nil
}
// ParseBody parses the message body and extracts the data based on the patterns defined in the configuration.
func ParseBody(messageBody string, config *config.Config) map[string]string {
if body := FindPatterns(messageBody, config); 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
}
+66
View File
@@ -0,0 +1,66 @@
package parsers
import (
"caatsm/internal/config"
"regexp"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestParsers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Parsers Suite")
}
var _ = Describe("Pattern Parser", func() {
var testConfig *config.Config
BeforeEach(func() {
testConfig = &config.Config{
Body: []config.BodyConfig{
{
Name: "FPL",
Patterns: []config.PatternConfig{
{
Pattern: `^\((?P<type>[A-Z]{3})-(?P<number>[A-Z0-9]+)\)$`,
Expression: regexp.MustCompile(`^\((?P<type>[A-Z]{3})-(?P<number>[A-Z0-9]+)\)$`),
},
},
},
},
}
})
Describe("FindPatterns", func() {
It("should return the correct BodyConfig based on the message body", func() {
message := "(FPL-AB123)"
bodyConfig := FindPatterns(message, testConfig)
Expect(bodyConfig).NotTo(BeNil())
Expect(bodyConfig.Name).To(Equal("FPL"))
})
It("should return nil if no pattern matches", func() {
message := "(XYZ-123)"
bodyConfig := FindPatterns(message, testConfig)
Expect(bodyConfig).To(BeNil())
})
})
Describe("ParseBody", func() {
It("should parse the message body and extract data based on patterns", func() {
message := "(FPL-AB123)"
parsedData := ParseBody(message, testConfig)
Expect(parsedData).NotTo(BeNil())
Expect(parsedData["type"]).To(Equal("FPL"))
Expect(parsedData["number"]).To(Equal("AB123"))
})
It("should return nil if no patterns match", func() {
message := "(XYZ-123)"
parsedData := ParseBody(message, testConfig)
Expect(parsedData).To(BeNil())
})
})
})