Files
go-caatsm/internal/parsers/aviation_parser.go
T
windyboy 496dc2b9d0 refactor: Update go.mod dependencies
This commit updates the go.mod file to include the latest versions of the following dependencies:
- github.com/Khan/genqlient v0.7.0
- github.com/google/uuid v1.6.0

These updates ensure that the project is using the most recent and stable versions of the dependencies, improving the overall reliability and functionality of the code.
2024-07-24 10:33:12 +08:00

292 lines
9.1 KiB
Go

package parsers
import (
"caatsm/internal/config"
"caatsm/internal/domain"
"fmt"
"regexp"
"strings"
"time"
)
const (
StartIndicatorPrefix = "ZCZC"
EndHeaderMarker = "."
BeginPartMarker = "BEGIN PART"
)
var (
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>.*)$`)
remarkPattern = regexp.MustCompile(`(?s)RMK\/(?P<remark>.*)`)
selPattern = regexp.MustCompile(`(?m)SEL\/(?P<sel>\w+)`)
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}\/`)
otherPatterns = []*regexp.Regexp{navPattern, remarkPattern, selPattern, pbnPattern, eetPattern, performancePattern, reroutePattern}
)
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) (string, interface{}, error) {
body = strings.TrimSpace(body)
category := findCategory(body)
if category == "" {
return "", nil, fmt.Errorf("no category found in body text")
}
if patternConfig, exists := bp.bodyPatterns[category]; exists && patternConfig.Patterns != nil {
for _, p := range patternConfig.Patterns {
if match := p.Expression.FindStringSubmatch(body); match != nil {
data := extractData(match, p.Expression)
return createBodyData(data)
}
}
}
return "", nil, fmt.Errorf("no matching pattern found for body: %s", body)
}
// findCategory extracts the category from the body text using regex.
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 ""
}
// extractData extracts named groups from the regex match and returns them as a map.
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) (string, interface{}, error) {
switch category := data["category"]; category {
case "ARR":
return category, &domain.ARR{
Category: data["category"],
AircraftID: data["number"],
SSRModeAndCode: data["ssr"],
DepartureAirport: data["departure"],
ArrivalAirport: data["arrival"],
ArrivalTime: data["time"],
}, nil
case "DEP":
return category, &domain.DEP{
Category: data["category"],
AircraftID: data["number"],
SSRModeAndCode: data["ssr"],
DepartureAirport: data["departure"],
DepartureTime: data["departure_time"],
Destination: data["arrival"],
}, nil
case "FPL":
otherData := parseOther(data["other"])
return category, &domain.FPL{
Category: data["category"],
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: data["other"],
Register: otherData["reg"],
EstimatedArrivalTime: data["estt"],
PBN: otherData["pbn"],
NavigationEquipment: otherData["nav"],
EstimatedElapsedTime: otherData["eet"],
SELCALCode: otherData["sel"],
PerformanceCategory: otherData["per"],
RerouteInformation: otherData["rif"],
Remarks: otherData["remark"],
}, nil
default:
return category, nil, fmt.Errorf("cann't parse : %s", category)
}
}
// 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()
category, bodyData, err := bodyParser.Parse(message.BodyAndFooter)
message.Category = category
message.ParsedAt = time.Now()
if err != nil {
return &message, err
}
message.BodyData = bodyData
return &message, nil
}
// clean removes empty lines from a given text and extracts the body only.
func clean(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 the header of the message and returns a ParsedMessage struct.
func ParseHeader(fullMessage string) (domain.ParsedMessage, error) {
fullMessage = clean(fullMessage)
lines := strings.Split(fullMessage, "\n")
_, messageID, dateTime, err := parseStartIndicator(lines[0])
if err != nil {
return domain.ParsedMessage{}, err
}
priorityIndicator, primaryAddress := parsePriorityAndPrimary(lines[1])
secondaryAddresses, originator, originatorDateTime, bodyAndFooter := parseRemainingLines(lines[2:])
return domain.ParsedMessage{
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) {
parts := strings.Fields(line)
if len(parts) >= 2 {
return parts[0], parts[1]
}
return "", ""
}
// 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
if strings.Index(line, "NNNN") > 0 {
break
}
bodyAndFooter.WriteString(line + "\n")
default:
if o1, o2 := getOriginator(line); o1 != "" {
originatorDateTime = o1
originator = o2
} else {
secondaryAddresses = append(secondaryAddresses, line)
}
}
}
}
return secondaryAddresses, originator, originatorDateTime, bodyAndFooter.String()
}
// getOriginator extracts originator details from a line of text.
func getOriginator(line string) (string, string) {
match := originator.FindStringSubmatch(line)
if len(match) >= 3 {
return match[1], match[2]
}
return "", ""
}
// parseOther parses additional information from the message body.
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
}