Files
go-caatsm/internal/parsers/aviation_parser.go
T
windyboy e8be157bd3 refactor: Update AFTN message parsing logic
The code changes in `aviation_parser_test.go` update the AFTN message parsing logic. The `ParseHeader` function now returns an additional error value, allowing for better error handling. The tests have been updated to reflect this change, ensuring that the parsing function handles errors correctly. This refactor improves the reliability and maintainability of the AFTN message parsing functionality.
2024-07-20 22:33:22 +08:00

220 lines
6.7 KiB
Go

package parsers
import (
"caatsm/internal/config"
"caatsm/internal/domain"
"caatsm/pkg/utils"
"fmt"
"regexp"
"strings"
"time"
)
const (
StartIndicatorPrefix = "ZCZC"
EndHeaderMarker = "."
BeginPartMarker = "BEGIN PART"
)
type BodyParser struct {
bodyPatterns map[string]config.BodyConfig
}
// NewBodyParser initializes a BodyParser with the default body patterns.
func NewBodyParser() *BodyParser {
return &BodyParser{bodyPatterns: config.GetBodyPatterns()}
}
// GetBodyPatterns returns the body patterns used by the parser.
func (bp *BodyParser) GetBodyPatterns() map[string]config.BodyConfig {
return bp.bodyPatterns
}
// SetBodyPatterns sets the body patterns for the parser.
func (bp *BodyParser) SetBodyPatterns(patterns map[string]config.BodyConfig) {
bp.bodyPatterns = patterns
}
// Parse attempts to parse the body text using the configured patterns.
func (bp *BodyParser) Parse(body string) (interface{}, error) {
log := utils.Logger
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)
data := extractData(match, re)
return createBodyData(data)
}
log.Debugf("No match for pattern %d\n", i)
}
}
return nil, fmt.Errorf("no matching pattern found for body: %s", body)
}
// extractData extracts named groups from the regex match.
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] = match[i]
}
}
return data
}
// createBodyData creates the appropriate domain object based on the type of message.
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"])
}
}
// Parse parses the raw text message and returns a ParsedMessage.
func Parse(rawText string) (*domain.ParsedMessage, error) {
message, err := ParseHeader(rawText)
if err != nil {
return nil, err
}
bodyParser := NewBodyParser()
bodyData, err := bodyParser.Parse(message.BodyAndFooter)
if err != nil {
return nil, err
}
message.ParsedAt = time.Now()
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)
lines := strings.Split(fullMessage, "\n")
startIndicator, messageID, dateTime, err := parseStartIndicator(lines[0])
if err != nil {
return domain.ParsedMessage{}, err
}
priorityIndicator, primaryAddress, err := parsePriorityAndPrimary(lines[1])
if err != nil {
return domain.ParsedMessage{}, err
}
secondaryAddresses, originator, originatorDateTime, bodyAndFooter := parseRemainingLines(lines[2:])
return domain.ParsedMessage{
StartIndicator: startIndicator,
MessageID: messageID,
DateTime: dateTime,
PriorityIndicator: priorityIndicator,
PrimaryAddress: primaryAddress,
SecondaryAddresses: secondaryAddresses,
Originator: originator,
OriginatorDateTime: originatorDateTime,
BodyAndFooter: bodyAndFooter,
ReceivedAt: time.Now(),
}, nil
}
// parseStartIndicator parses the start indicator line.
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
}
return "", "", "", fmt.Errorf("invalid start indicator line format: %s", line)
}
// parsePriorityAndPrimary parses the priority indicator and primary address line.
func parsePriorityAndPrimary(line string) (string, string, error) {
parts := strings.Fields(line)
if len(parts) >= 2 {
return parts[0], parts[1], nil
}
return "", "", fmt.Errorf("invalid priority indicator line format: %s", line)
}
// parseRemainingLines parses the remaining lines of the message.
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:
// End header marker, do nothing
case strings.HasPrefix(line, "."):
originatorInfo := strings.Fields(line[1:])
if len(originatorInfo) >= 2 {
originator = originatorInfo[0]
originatorDateTime = originatorInfo[1]
}
headerEnded = true
case strings.HasPrefix(line, BeginPartMarker) || strings.HasPrefix(line, "("):
headerEnded = true
bodyAndFooter.WriteString(line + "\n")
default:
secondaryAddresses = append(secondaryAddresses, line)
}
}
}
return secondaryAddresses, originator, originatorDateTime, bodyAndFooter.String()
}