Add configuration for Code Review Automation and enhance .gitignore. Introduce .coderabbit.yml for automated reviews with profiles for correctness, maintainability, security, and performance. Update paths to include relevant directories and exclude generated files. Modify .gitignore to include coverage reports and generated files. Refactor Docker Compose to use updated paths for database initialization scripts. Update Go module dependencies and enhance Makefile with new code generation tasks. Transition domain models to a new DTO structure for better separation of concerns.

This commit is contained in:
windyboy
2025-11-17 11:47:23 +08:00
parent 12fda00df9
commit 204217015d
45 changed files with 252 additions and 175 deletions
+408
View File
@@ -0,0 +1,408 @@
package parser
import (
"caatsm/internal/domain"
"caatsm/internal/adapter/dto"
"errors"
"fmt"
"regexp"
"strings"
"sync"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
)
const (
SSR = "ssr"
DepartureCode = "dep"
DepartureTime = "dep_time"
ArrivalCode = "arr"
ArrivalTime = "arr_time"
DestinationCode = "dest"
OtherInfo = "other"
ReferenceData = "reference_data"
Aircraft = "aircraft"
CategorySurveillance = "surve"
Indicator = "indicator"
Other = "other"
AircraftID = "aircraft"
Surveillance = "surve"
Speed = "speed"
Level = "level"
Route = "route"
EstimatedTime = "estt"
AlternateAirport = "alter"
PBN = "pbn"
NavigationEquipment = "nav"
EstimatedElapsedTime = "eet"
SELCALCode = "sel"
PerformanceCategory = "per"
RerouteInformation = "rif"
Remarks = "remark"
)
var (
otherPatterns = []*regexp.Regexp{navPattern,
remarkPattern,
selPattern,
pbnPattern,
eetPattern,
performancePattern,
regPattern,
reroutePattern}
// ErrHeaderParse indicates an invalid header section.
ErrHeaderParse = errors.New("invalid telegram header")
// ErrBodyParse indicates a failure matching the telegram body.
ErrBodyParse = errors.New("invalid telegram body")
)
type BodyParser struct {
body string
bodyPatterns map[string]BodyConfig
mu sync.Mutex
}
func NewBodyParser(body string) *BodyParser {
return &BodyParser{
bodyPatterns: bodyPatterns,
body: body,
}
}
func (parser *BodyParser) GetBodyPatterns() map[string]BodyConfig {
parser.mu.Lock()
defer parser.mu.Unlock()
return parser.bodyPatterns
}
func (parser *BodyParser) SetBodyPatterns(patterns map[string]BodyConfig) {
parser.mu.Lock()
defer parser.mu.Unlock()
parser.bodyPatterns = patterns
}
func (parser *BodyParser) Parse() (string, interface{}, error) {
parser.mu.Lock()
defer parser.mu.Unlock()
parser.body = strings.TrimSpace(parser.body)
category := findCategory(parser.body)
if category == "" {
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)
}
}
}
return "", nil, fmt.Errorf("no matching pattern found for body: %s", parser.body)
}
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 ""
}
func extract(data string, exp *regexp.Regexp) map[string]string {
match := exp.FindStringSubmatch(data)
if len(match) > 0 {
return extractData(match, exp)
}
return nil
}
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] = strings.TrimSpace(match[i])
}
}
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 Parse(rawText string) (*dto.ParsedTelegram, error) {
header, err := ParseHeader(rawText)
if err != nil {
msg := dto.NewParsedTelegram()
msg.Content = rawText
msg.Comments = err.Error()
msg.ErrorReason = err.Error()
msg.Status = dto.MessageStatusHeaderError
return msg, fmt.Errorf("%w: %w", ErrHeaderParse, err)
}
bodyParser := NewBodyParser(header.Body)
category, bodyData, bodyErr := bodyParser.Parse()
header.Category = category
header.ParsedAt = time.Now()
if bodyErr != nil {
return &dto.ParsedTelegram{
MessageID: header.MessageID,
DateTime: header.DateTime,
PriorityIndicator: header.PriorityIndicator,
PrimaryAddress: header.PrimaryAddress,
SecondaryAddresses: header.SecondaryAddresses,
Originator: header.Originator,
OriginatorDateTime: header.OriginatorDateTime,
Category: header.Category,
Body: header.Body,
Content: header.Content,
ReceivedAt: header.ReceivedAt,
ParsedAt: header.ParsedAt,
Parsed: false,
Comments: bodyErr.Error(),
Status: dto.MessageStatusBodyError,
ErrorReason: bodyErr.Error(),
}, fmt.Errorf("%w: %w", ErrBodyParse, bodyErr)
}
parsed := &dto.ParsedTelegram{
MessageID: header.MessageID,
DateTime: header.DateTime,
PriorityIndicator: header.PriorityIndicator,
PrimaryAddress: header.PrimaryAddress,
SecondaryAddresses: header.SecondaryAddresses,
Originator: header.Originator,
OriginatorDateTime: header.OriginatorDateTime,
Category: header.Category,
Body: header.Body,
Content: header.Content,
BodyData: bodyData,
ReceivedAt: header.ReceivedAt,
ParsedAt: header.ParsedAt,
Parsed: true,
Status: dto.MessageStatusParsed,
ErrorReason: "",
}
parsed.Uuid = uuid.New().String()
return parsed, nil
}
func cleanMessage(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 only the header portion of the message and returns a lightweight struct
// with header fields and body content. It is used internally by the aviation parser.
type Header struct {
MessageID string
DateTime string
PriorityIndicator string
PrimaryAddress string
SecondaryAddresses string
Originator string
OriginatorDateTime string
Category string
Content string
Body string
ReceivedAt time.Time
ParsedAt time.Time
}
func ParseHeader(fullMessage string) (Header, error) {
log := zap.S()
cleaned := cleanMessage(fullMessage)
lines := strings.Split(cleaned, "\n")
if len(lines) < 3 {
log.Warnf("invalid message format: %s", fullMessage)
return Header{Content: fullMessage}, fmt.Errorf("invalid message format: %s", fullMessage)
}
_, messageID, dateTime, err := parseStartIndicator(lines[0])
if err != nil {
return Header{Content: fullMessage}, err
}
priorityIndicator, primaryAddress := parsePriorityAndPrimary(lines[1])
secondaryAddresses, originator, originatorDateTime, body := parseRemainingLines(lines[2:])
return Header{
MessageID: messageID,
DateTime: dateTime,
PriorityIndicator: priorityIndicator,
PrimaryAddress: primaryAddress,
SecondaryAddresses: secondaryAddresses,
Originator: originator,
OriginatorDateTime: originatorDateTime,
Content: fullMessage,
Body: body,
ReceivedAt: time.Now(),
}, nil
}
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
}
zap.S().Warnf("invalid start indicator line format: %s", line)
return "", "", "", fmt.Errorf("invalid start indicator line format: %s", line)
}
func parsePriorityAndPrimary(line string) (string, string) {
parts := strings.Fields(line)
if len(parts) >= 2 {
return parts[0], parts[1]
}
zap.S().Warnf("invalid priority and primary address line format: %s", line)
return "", ""
}
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:
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 = secondaryAddresses + " " + line
}
}
}
}
return secondaryAddresses, originator, originatorDateTime, bodyAndFooter.String()
}
func getOriginator(line string) (string, string) {
match := originator.FindStringSubmatch(line)
if len(match) >= 3 {
return match[1], match[2]
}
zap.S().Warnf("invalid originator line format: %s", line)
return "", ""
}
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
}