refactor aviation parser architecture
This commit is contained in:
@@ -2,7 +2,6 @@ package aviation
|
||||
|
||||
import (
|
||||
"caatsm/internal/adapter/dto"
|
||||
"caatsm/internal/domain"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
@@ -68,13 +67,26 @@ func (parser *BodyParser) Parse() (string, interface{}, error) {
|
||||
return "", nil, fmt.Errorf("no category found in body text")
|
||||
}
|
||||
|
||||
if patternConfig, exists := parser.bodyPatterns[category]; exists && patternConfig.Patterns != nil {
|
||||
for _, p := range patternConfig.Patterns {
|
||||
if data := extract(parser.body, p.Expression); data != nil {
|
||||
return parser.createBodyData(data)
|
||||
patternConfig, exists := parser.bodyPatterns[category]
|
||||
if !exists || patternConfig.Patterns == nil {
|
||||
return "", nil, fmt.Errorf("no matching pattern found for body: %s", parser.body)
|
||||
}
|
||||
|
||||
ctx := ParseContext{
|
||||
Body: parser.body,
|
||||
Tokens: Tokenizer{}.Tokenize(parser.body),
|
||||
}
|
||||
|
||||
for _, p := range patternConfig.Patterns {
|
||||
if data := extract(parser.body, p.Expression); data != nil {
|
||||
parsed, err := parseCategory(category, ctx, data)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return category, parsed, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil, fmt.Errorf("no matching pattern found for body: %s", parser.body)
|
||||
}
|
||||
|
||||
@@ -107,73 +119,6 @@ func extractData(match []string, re *regexp.Regexp) map[string]string {
|
||||
return data
|
||||
}
|
||||
|
||||
func (parser *BodyParser) createBodyData(data map[string]string) (string, interface{}, error) {
|
||||
switch category := data["category"]; category {
|
||||
case CategoryArrival:
|
||||
return category, &domain.ARR{
|
||||
Category: data[Category],
|
||||
AircraftID: data[FlightNumber],
|
||||
SSRModeAndCode: data[SSR],
|
||||
DepartureAirport: data[DepartureCode],
|
||||
ArrivalAirport: data[ArrivalCode],
|
||||
ArrivalTime: data[ArrivalTime],
|
||||
}, nil
|
||||
case CategoryDeparture:
|
||||
return category, &domain.DEP{
|
||||
Category: data[Category],
|
||||
AircraftID: data[FlightNumber],
|
||||
SSRModeAndCode: data[SSR],
|
||||
DepartureAirport: data[DepartureCode],
|
||||
DepartureTime: data[DepartureTime],
|
||||
Destination: data[ArrivalCode],
|
||||
}, nil
|
||||
case CategoryCancellation:
|
||||
return category, &domain.CNL{
|
||||
Category: data[Category],
|
||||
AircraftID: data[FlightNumber],
|
||||
DepartureAirport: data[DepartureCode],
|
||||
DestinationAirport: data[ArrivalCode],
|
||||
}, nil
|
||||
case CategoryDelay:
|
||||
return category, &domain.DLA{
|
||||
Category: data[Category],
|
||||
AircraftID: data[FlightNumber],
|
||||
DepartureAirport: data[DepartureCode],
|
||||
NewDepartureTime: data[DepartureTime],
|
||||
ArrivalAirport: data[ArrivalCode],
|
||||
ArrivalTime: data[ArrivalTime],
|
||||
}, nil
|
||||
case CategoryFlightPlan:
|
||||
otherData := parseOther(data[OtherInfo])
|
||||
return category, &domain.FPL{
|
||||
Category: data[Category],
|
||||
FlightNumber: data[FlightNumber],
|
||||
ReferenceData: data[ReferenceData],
|
||||
AircraftID: data[AircraftID],
|
||||
SSRModeAndCode: data[Surveillance],
|
||||
FlightRulesAndType: data[Indicator],
|
||||
CruisingSpeedAndLevel: data[Speed] + data[Level],
|
||||
DepartureAirport: data[DepartureCode],
|
||||
DepartureTime: data[DepartureTime],
|
||||
Route: data[Route],
|
||||
DestinationAndTotalTime: data[DestinationCode] + data[EstimatedTime],
|
||||
AlternateAirport: data[AlternateAirport],
|
||||
OtherInfo: data[OtherInfo],
|
||||
Register: otherData[Register],
|
||||
EstimatedArrivalTime: data[EstimatedTime],
|
||||
PBN: otherData[PBN],
|
||||
NavigationEquipment: otherData[NavigationEquipment],
|
||||
EstimatedElapsedTime: otherData[EstimatedElapsedTime],
|
||||
SELCALCode: otherData[SELCALCode],
|
||||
PerformanceCategory: otherData[PerformanceCategory],
|
||||
RerouteInformation: otherData[RerouteInformation],
|
||||
Remarks: otherData[Remarks],
|
||||
}, nil
|
||||
default:
|
||||
return category, nil, fmt.Errorf("invalid message type: %s", category)
|
||||
}
|
||||
}
|
||||
|
||||
func headerToParsedTelegram(header Header) dto.ParsedTelegram {
|
||||
return dto.ParsedTelegram{
|
||||
MessageID: header.MessageID,
|
||||
|
||||
@@ -15,60 +15,9 @@ type PatternConfig struct {
|
||||
}
|
||||
|
||||
var (
|
||||
bodyPatterns = map[string]BodyConfig{}
|
||||
bodyPatterns = buildBodyPatterns()
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize body patterns.
|
||||
bodyPatterns = map[string]BodyConfig{
|
||||
"ARR": {
|
||||
Patterns: []PatternConfig{
|
||||
{
|
||||
Pattern: ArrPatternString,
|
||||
Comments: "Pattern for ARR message",
|
||||
Expression: ArrPatternExpression,
|
||||
},
|
||||
},
|
||||
},
|
||||
"DEP": {
|
||||
Patterns: []PatternConfig{
|
||||
{
|
||||
Pattern: DepPatternString,
|
||||
Comments: "Pattern for DEP message",
|
||||
Expression: DepPatternExpression,
|
||||
},
|
||||
},
|
||||
},
|
||||
"FPL": {
|
||||
Patterns: []PatternConfig{
|
||||
{
|
||||
Pattern: FplPatternString,
|
||||
Comments: "Pattern for FPL message",
|
||||
Expression: FplPatternExpression,
|
||||
},
|
||||
},
|
||||
},
|
||||
"CNL": {
|
||||
Patterns: []PatternConfig{
|
||||
{
|
||||
Pattern: CnlPatternString,
|
||||
Comments: "Pattern for CNL message",
|
||||
Expression: CnlPatternExpression,
|
||||
},
|
||||
},
|
||||
},
|
||||
"DLA": {
|
||||
Patterns: []PatternConfig{
|
||||
{
|
||||
Pattern: DlaPatternString,
|
||||
Comments: "Pattern for DLA message",
|
||||
Expression: DlaPatternExpression,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// FindPatterns finds the matching body configuration based on the message body.
|
||||
func FindPatterns(messageBody string) *BodyConfig {
|
||||
if match := BodyTypePattern.FindStringSubmatch(messageBody); len(match) > 1 {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package aviation
|
||||
|
||||
import (
|
||||
"caatsm/internal/domain"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ParseContext carries the raw body and tokens for field parsers.
|
||||
type ParseContext struct {
|
||||
Body string
|
||||
Tokens []Token
|
||||
}
|
||||
|
||||
// CategoryParser maps a category to patterns and output parsing.
|
||||
type CategoryParser interface {
|
||||
Category() string
|
||||
Patterns() []PatternConfig
|
||||
Parse(ctx ParseContext, data map[string]string) (interface{}, error)
|
||||
}
|
||||
|
||||
type arrParser struct{}
|
||||
|
||||
func (arrParser) Category() string { return CategoryArrival }
|
||||
|
||||
func (arrParser) Patterns() []PatternConfig {
|
||||
return []PatternConfig{
|
||||
{
|
||||
Pattern: ArrPatternString,
|
||||
Comments: "Pattern for ARR message",
|
||||
Expression: ArrPatternExpression,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (arrParser) Parse(_ ParseContext, data map[string]string) (interface{}, error) {
|
||||
return &domain.ARR{
|
||||
Category: data[Category],
|
||||
AircraftID: data[FlightNumber],
|
||||
SSRModeAndCode: data[SSR],
|
||||
DepartureAirport: data[DepartureCode],
|
||||
ArrivalAirport: data[ArrivalCode],
|
||||
ArrivalTime: data[ArrivalTime],
|
||||
}, nil
|
||||
}
|
||||
|
||||
type depParser struct{}
|
||||
|
||||
func (depParser) Category() string { return CategoryDeparture }
|
||||
|
||||
func (depParser) Patterns() []PatternConfig {
|
||||
return []PatternConfig{
|
||||
{
|
||||
Pattern: DepPatternString,
|
||||
Comments: "Pattern for DEP message",
|
||||
Expression: DepPatternExpression,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (depParser) Parse(_ ParseContext, data map[string]string) (interface{}, error) {
|
||||
return &domain.DEP{
|
||||
Category: data[Category],
|
||||
AircraftID: data[FlightNumber],
|
||||
SSRModeAndCode: data[SSR],
|
||||
DepartureAirport: data[DepartureCode],
|
||||
DepartureTime: data[DepartureTime],
|
||||
Destination: data[ArrivalCode],
|
||||
}, nil
|
||||
}
|
||||
|
||||
type cnlParser struct{}
|
||||
|
||||
func (cnlParser) Category() string { return CategoryCancellation }
|
||||
|
||||
func (cnlParser) Patterns() []PatternConfig {
|
||||
return []PatternConfig{
|
||||
{
|
||||
Pattern: CnlPatternString,
|
||||
Comments: "Pattern for CNL message",
|
||||
Expression: CnlPatternExpression,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cnlParser) Parse(_ ParseContext, data map[string]string) (interface{}, error) {
|
||||
return &domain.CNL{
|
||||
Category: data[Category],
|
||||
AircraftID: data[FlightNumber],
|
||||
DepartureAirport: data[DepartureCode],
|
||||
DestinationAirport: data[ArrivalCode],
|
||||
}, nil
|
||||
}
|
||||
|
||||
type dlaParser struct{}
|
||||
|
||||
func (dlaParser) Category() string { return CategoryDelay }
|
||||
|
||||
func (dlaParser) Patterns() []PatternConfig {
|
||||
return []PatternConfig{
|
||||
{
|
||||
Pattern: DlaPatternString,
|
||||
Comments: "Pattern for DLA message",
|
||||
Expression: DlaPatternExpression,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (dlaParser) Parse(_ ParseContext, data map[string]string) (interface{}, error) {
|
||||
return &domain.DLA{
|
||||
Category: data[Category],
|
||||
AircraftID: data[FlightNumber],
|
||||
DepartureAirport: data[DepartureCode],
|
||||
NewDepartureTime: data[DepartureTime],
|
||||
ArrivalAirport: data[ArrivalCode],
|
||||
ArrivalTime: data[ArrivalTime],
|
||||
}, nil
|
||||
}
|
||||
|
||||
type fplParser struct{}
|
||||
|
||||
func (fplParser) Category() string { return CategoryFlightPlan }
|
||||
|
||||
func (fplParser) Patterns() []PatternConfig {
|
||||
return []PatternConfig{
|
||||
{
|
||||
Pattern: FplPatternString,
|
||||
Comments: "Pattern for FPL message",
|
||||
Expression: FplPatternExpression,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (fplParser) Parse(_ ParseContext, data map[string]string) (interface{}, error) {
|
||||
otherData := parseOther(data[OtherInfo])
|
||||
return &domain.FPL{
|
||||
Category: data[Category],
|
||||
FlightNumber: data[FlightNumber],
|
||||
ReferenceData: data[ReferenceData],
|
||||
AircraftID: data[AircraftID],
|
||||
SSRModeAndCode: data[Surveillance],
|
||||
FlightRulesAndType: data[Indicator],
|
||||
CruisingSpeedAndLevel: data[Speed] + data[Level],
|
||||
DepartureAirport: data[DepartureCode],
|
||||
DepartureTime: data[DepartureTime],
|
||||
Route: data[Route],
|
||||
DestinationAndTotalTime: data[DestinationCode] + data[EstimatedTime],
|
||||
AlternateAirport: data[AlternateAirport],
|
||||
OtherInfo: data[OtherInfo],
|
||||
Register: otherData[Register],
|
||||
EstimatedArrivalTime: data[EstimatedTime],
|
||||
PBN: otherData[PBN],
|
||||
NavigationEquipment: otherData[NavigationEquipment],
|
||||
EstimatedElapsedTime: otherData[EstimatedElapsedTime],
|
||||
SELCALCode: otherData[SELCALCode],
|
||||
PerformanceCategory: otherData[PerformanceCategory],
|
||||
RerouteInformation: otherData[RerouteInformation],
|
||||
Remarks: otherData[Remarks],
|
||||
}, nil
|
||||
}
|
||||
|
||||
var categoryRegistry = map[string]CategoryParser{
|
||||
CategoryArrival: arrParser{},
|
||||
CategoryDeparture: depParser{},
|
||||
CategoryCancellation: cnlParser{},
|
||||
CategoryDelay: dlaParser{},
|
||||
CategoryFlightPlan: fplParser{},
|
||||
}
|
||||
|
||||
func lookupCategoryParser(category string) (CategoryParser, bool) {
|
||||
parser, ok := categoryRegistry[category]
|
||||
return parser, ok
|
||||
}
|
||||
|
||||
func buildBodyPatterns() map[string]BodyConfig {
|
||||
patterns := make(map[string]BodyConfig, len(categoryRegistry))
|
||||
for category, parser := range categoryRegistry {
|
||||
patterns[category] = BodyConfig{Patterns: parser.Patterns()}
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
func parseCategory(category string, ctx ParseContext, data map[string]string) (interface{}, error) {
|
||||
parser, ok := lookupCategoryParser(category)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid message type: %s", category)
|
||||
}
|
||||
return parser.Parse(ctx, data)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package aviation
|
||||
|
||||
import "strings"
|
||||
|
||||
// Token represents a lexeme in the body with its byte offsets.
|
||||
type Token struct {
|
||||
Text string
|
||||
Start int
|
||||
End int
|
||||
}
|
||||
|
||||
// Tokenizer splits text into tokens using a whitespace set.
|
||||
// A forward slash is treated as whitespace but is emitted as its own token.
|
||||
type Tokenizer struct {
|
||||
Whitespace string
|
||||
}
|
||||
|
||||
// Tokenize tokenizes input and returns tokens with byte offsets.
|
||||
func (t Tokenizer) Tokenize(input string) []Token {
|
||||
if t.Whitespace == "" {
|
||||
t.Whitespace = " \n\t\r"
|
||||
}
|
||||
|
||||
var tokens []Token
|
||||
start := -1
|
||||
|
||||
for idx, r := range input {
|
||||
if strings.ContainsRune(t.Whitespace, r) {
|
||||
if start != -1 {
|
||||
tokens = append(tokens, Token{
|
||||
Text: input[start:idx],
|
||||
Start: start,
|
||||
End: idx,
|
||||
})
|
||||
start = -1
|
||||
}
|
||||
if r == '/' {
|
||||
tokens = append(tokens, Token{
|
||||
Text: "/",
|
||||
Start: idx,
|
||||
End: idx + 1,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if start == -1 {
|
||||
start = idx
|
||||
}
|
||||
}
|
||||
|
||||
if start != -1 {
|
||||
tokens = append(tokens, Token{
|
||||
Text: input[start:],
|
||||
Start: start,
|
||||
End: len(input),
|
||||
})
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package aviation
|
||||
|
||||
import (
|
||||
"caatsm/internal/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTokenizerDefaultWhitespace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := "A B\nC\tD\rE"
|
||||
tokens := Tokenizer{}.Tokenize(input)
|
||||
|
||||
expected := []Token{
|
||||
{Text: "A", Start: 0, End: 1},
|
||||
{Text: "B", Start: 2, End: 3},
|
||||
{Text: "C", Start: 4, End: 5},
|
||||
{Text: "D", Start: 6, End: 7},
|
||||
{Text: "E", Start: 8, End: 9},
|
||||
}
|
||||
|
||||
if len(tokens) != len(expected) {
|
||||
t.Fatalf("expected %d tokens, got %d", len(expected), len(tokens))
|
||||
}
|
||||
|
||||
for i, token := range tokens {
|
||||
if token != expected[i] {
|
||||
t.Fatalf("token %d mismatch: got %#v, expected %#v", i, token, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenizerSlashWhitespace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := "A/B C"
|
||||
tokens := Tokenizer{Whitespace: " \n\t\r/"}.Tokenize(input)
|
||||
|
||||
expected := []Token{
|
||||
{Text: "A", Start: 0, End: 1},
|
||||
{Text: "/", Start: 1, End: 2},
|
||||
{Text: "B", Start: 2, End: 3},
|
||||
{Text: "C", Start: 4, End: 5},
|
||||
}
|
||||
|
||||
if len(tokens) != len(expected) {
|
||||
t.Fatalf("expected %d tokens, got %d", len(expected), len(tokens))
|
||||
}
|
||||
|
||||
for i, token := range tokens {
|
||||
if token != expected[i] {
|
||||
t.Fatalf("token %d mismatch: got %#v, expected %#v", i, token, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBodyPatternsIncludesCategories(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
patterns := buildBodyPatterns()
|
||||
for _, category := range []string{
|
||||
CategoryArrival,
|
||||
CategoryDeparture,
|
||||
CategoryCancellation,
|
||||
CategoryDelay,
|
||||
CategoryFlightPlan,
|
||||
} {
|
||||
config, ok := patterns[category]
|
||||
if !ok {
|
||||
t.Fatalf("expected category %s in body patterns", category)
|
||||
}
|
||||
if len(config.Patterns) == 0 || config.Patterns[0].Expression == nil {
|
||||
t.Fatalf("expected pattern expression for category %s", category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCategoryInvalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := parseCategory("XYZ", ParseContext{}, map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid category")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCategoryFPL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)`
|
||||
|
||||
data := extract(body, FplPatternExpression)
|
||||
if data == nil {
|
||||
t.Fatal("expected FPL pattern to match")
|
||||
}
|
||||
|
||||
parsed, err := parseCategory(CategoryFlightPlan, ParseContext{
|
||||
Body: body,
|
||||
Tokens: Tokenizer{}.Tokenize(body),
|
||||
}, data)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parse error: %v", err)
|
||||
}
|
||||
|
||||
fpl, ok := parsed.(*domain.FPL)
|
||||
if !ok {
|
||||
t.Fatalf("expected *domain.FPL, got %T", parsed)
|
||||
}
|
||||
|
||||
if fpl.FlightNumber != "CCA1532" {
|
||||
t.Fatalf("expected flight number CCA1532, got %s", fpl.FlightNumber)
|
||||
}
|
||||
if fpl.PBN != "A1B2B3B4B5D1L1" {
|
||||
t.Fatalf("expected PBN A1B2B3B4B5D1L1, got %s", fpl.PBN)
|
||||
}
|
||||
if fpl.RerouteInformation != "FRT N640 ZBYN" {
|
||||
t.Fatalf("expected reroute information, got %s", fpl.RerouteInformation)
|
||||
}
|
||||
if fpl.Remarks != "TCAS EQUIPPED" {
|
||||
t.Fatalf("expected remarks TCAS EQUIPPED, got %s", fpl.Remarks)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user