init
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# Variables
|
||||
APP_NAME = tele-proc
|
||||
GO_FILES = $(shell find . -name '*.go' -type f)
|
||||
CONFIG_DIR = configs
|
||||
BUILD_DIR = build
|
||||
MAIN_RECEIVER = ./cmd/receiver/main.go
|
||||
|
||||
# Default target
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
# Build the receiver application
|
||||
.PHONY: build
|
||||
build: build-receiver
|
||||
|
||||
.PHONY: build-receiver
|
||||
build-receiver:
|
||||
@echo "Building receiver..."
|
||||
@go build -o $(BUILD_DIR)/receiver $(MAIN_RECEIVER)
|
||||
|
||||
# Run the receiver application with different configurations
|
||||
.PHONY: run
|
||||
run: run-dev
|
||||
|
||||
.PHONY: run-dev
|
||||
run-dev:
|
||||
@echo "Running receiver in development mode..."
|
||||
@GO_ENV=development $(BUILD_DIR)/receiver &
|
||||
|
||||
.PHONY: run-prod
|
||||
run-prod:
|
||||
@echo "Running receiver in production mode..."
|
||||
@GO_ENV=production $(BUILD_DIR)/receiver &
|
||||
|
||||
.PHONY: run-test
|
||||
run-test:
|
||||
@echo "Running receiver in test mode..."
|
||||
@GO_ENV=test $(BUILD_DIR)/receiver &
|
||||
|
||||
# Test the application
|
||||
.PHONY: test
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
@go test ./...
|
||||
|
||||
# Clean build artifacts
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
@rm -rf $(BUILD_DIR)
|
||||
|
||||
# Format the code
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
@echo "Formatting code..."
|
||||
@go fmt ./...
|
||||
|
||||
# Install dependencies
|
||||
.PHONY: deps
|
||||
deps:
|
||||
@echo "Installing dependencies..."
|
||||
@go mod tidy
|
||||
|
||||
# Lint the code
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@echo "Linting code..."
|
||||
@golangci-lint run
|
||||
|
||||
# Help
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Makefile usage:"
|
||||
@echo " make build - Build the application"
|
||||
@echo " make run - Run the receiver in development mode"
|
||||
@echo " make run-dev - Run the receiver in development mode"
|
||||
@echo " make run-prod - Run the receiver in production mode"
|
||||
@echo " make run-test - Run the receiver in test mode"
|
||||
@echo " make test - Run tests"
|
||||
@echo " make clean - Clean build artifacts"
|
||||
@echo " make fmt - Format the code"
|
||||
@echo " make deps - Install dependencies"
|
||||
@echo " make lint - Lint the code"
|
||||
@echo " make help - Show this help message"
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"caatsm/internal/config"
|
||||
"caatsm/pkg/utils"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
utils.Logger.WithError(err).Fatal("Error loading config")
|
||||
}
|
||||
|
||||
if err := config.ValidateConfig(cfg); err != nil {
|
||||
utils.Logger.WithError(err).Fatal("Config validation error")
|
||||
}
|
||||
|
||||
utils.Logger.Info("Loaded configuration successfully")
|
||||
|
||||
// Example usage
|
||||
utils.Logger.WithFields(logrus.Fields{
|
||||
"url": cfg.Nats.URL,
|
||||
}).Info("NATS configuration")
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"caatsm/pkg/utils"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
// Adjust this import based on your project structure
|
||||
)
|
||||
|
||||
var MyConfig *Config
|
||||
|
||||
type Config struct {
|
||||
Nats NatsConfig
|
||||
Subscription SubscriptionConfig
|
||||
Timeouts TimeoutsConfig
|
||||
Body []BodyConfig
|
||||
}
|
||||
|
||||
type NatsConfig struct {
|
||||
Client string
|
||||
URL string
|
||||
Cluster string
|
||||
}
|
||||
|
||||
type SubscriptionConfig struct {
|
||||
Topic string
|
||||
QueueGroup string `mapstructure:"queue_group"`
|
||||
}
|
||||
|
||||
type TimeoutsConfig struct {
|
||||
ServerTimeout string `mapstructure:"server_timeout"`
|
||||
ReconnectWait string `mapstructure:"reconnect_wait"`
|
||||
CloseTimeout string `mapstructure:"close_timeout"`
|
||||
AckWaitTimeout string `mapstructure:"ack_wait_timeout"`
|
||||
}
|
||||
|
||||
type BodyConfig struct {
|
||||
Name string
|
||||
Patterns []PatternConfig
|
||||
}
|
||||
|
||||
type PatternConfig struct {
|
||||
Pattern string
|
||||
Comments string
|
||||
Expression *regexp.Regexp
|
||||
}
|
||||
|
||||
func SetMyConfig(cfg *Config) {
|
||||
MyConfig = cfg
|
||||
}
|
||||
|
||||
func GetMyConfig() *Config {
|
||||
if MyConfig == nil {
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
utils.Logger.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
MyConfig = cfg
|
||||
}
|
||||
return MyConfig
|
||||
}
|
||||
|
||||
// LoadConfig loads the configuration from a file
|
||||
func LoadConfig() (*Config, error) {
|
||||
log := utils.Logger
|
||||
env := os.Getenv("GO_ENV")
|
||||
if env == "" {
|
||||
env = "dev"
|
||||
}
|
||||
log.Infof("Environment: %s", env)
|
||||
|
||||
viper.SetConfigType("toml")
|
||||
viper.SetConfigName("config." + env)
|
||||
viper.AddConfigPath("configs")
|
||||
viper.SetEnvPrefix("tele")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
errMsg := fmt.Sprintf("error reading config file for environment '%s': %v", env, err)
|
||||
log.Error(errMsg)
|
||||
return nil, fmt.Errorf(errMsg)
|
||||
}
|
||||
|
||||
log.Debug("Config file read successfully")
|
||||
log.Debugf("Config keys: %v", viper.AllKeys())
|
||||
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
errMsg := fmt.Sprintf("unable to decode config into struct for environment '%s': %v", env, err)
|
||||
log.Error(errMsg)
|
||||
return nil, fmt.Errorf(errMsg)
|
||||
}
|
||||
|
||||
log.Debugf("Config loaded before regex compilation: %+v", config)
|
||||
|
||||
// Compile regex patterns
|
||||
for i := range config.Body {
|
||||
for j := range config.Body[i].Patterns {
|
||||
name := config.Body[i].Name
|
||||
pattern := config.Body[i].Patterns[j].Pattern
|
||||
expr, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("error compiling regex for body '%s', pattern '%s': %v", name, pattern, err)
|
||||
log.Error(errMsg)
|
||||
return nil, fmt.Errorf(errMsg)
|
||||
}
|
||||
config.Body[i].Patterns[j].Expression = expr
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Final config after regex compilation: %+v", config)
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// ValidateConfig validates the loaded configuration
|
||||
func ValidateConfig(cfg *Config) error {
|
||||
log := utils.Logger
|
||||
|
||||
if cfg.Nats.Client == "" {
|
||||
err := "nats client is required"
|
||||
log.Error(err)
|
||||
return fmt.Errorf(err)
|
||||
}
|
||||
if cfg.Nats.URL == "" {
|
||||
err := "nats URL is required"
|
||||
log.Error(err)
|
||||
return fmt.Errorf(err)
|
||||
}
|
||||
if cfg.Subscription.Topic == "" {
|
||||
err := "subscription topic is required"
|
||||
log.Error(err)
|
||||
return fmt.Errorf(err)
|
||||
}
|
||||
if len(cfg.Body) == 0 {
|
||||
err := "at least one body configuration is required"
|
||||
log.Error(err)
|
||||
return fmt.Errorf(err)
|
||||
}
|
||||
for _, body := range cfg.Body {
|
||||
if body.Name == "" {
|
||||
err := "body name is required"
|
||||
log.Error(err)
|
||||
return fmt.Errorf(err)
|
||||
}
|
||||
for _, pattern := range body.Patterns {
|
||||
if pattern.Pattern == "" {
|
||||
err := fmt.Sprintf("pattern is required for body '%s'", body.Name)
|
||||
log.Error(err)
|
||||
return fmt.Errorf(err)
|
||||
}
|
||||
if pattern.Expression == nil {
|
||||
err := fmt.Sprintf("compiled expression is missing for pattern '%s' in body '%s'", pattern.Pattern, body.Name)
|
||||
log.Error(err)
|
||||
return fmt.Errorf(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Info("config validation passed")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Config Suite")
|
||||
}
|
||||
|
||||
var _ = Describe("Config", func() {
|
||||
var originalEnv string
|
||||
|
||||
BeforeEach(func() {
|
||||
// Save the original GO_ENV value
|
||||
originalEnv = os.Getenv("GO_ENV")
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// Restore the original GO_ENV value
|
||||
os.Setenv("GO_ENV", originalEnv)
|
||||
})
|
||||
|
||||
// Helper function to create temporary TOML files in the "configs" directory
|
||||
createTempConfigFile := func(env, content string) string {
|
||||
dir := "configs"
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to create config directory")
|
||||
|
||||
filename := filepath.Join(dir, "config."+env+".toml")
|
||||
tmpfile, err := os.Create(filename)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to create temp config file")
|
||||
_, err = tmpfile.Write([]byte(content))
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to write to temp config file")
|
||||
err = tmpfile.Close()
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to close temp config file")
|
||||
return filename
|
||||
}
|
||||
|
||||
Context("Loading and validating a valid TOML file", func() {
|
||||
It("should load and validate the configuration correctly", func() {
|
||||
content := `
|
||||
[nats]
|
||||
client = "test-client"
|
||||
url = "nats://localhost:4222"
|
||||
cluster = "test-cluster"
|
||||
|
||||
[subscription]
|
||||
topic = "example-topic"
|
||||
queue_group = "example-group"
|
||||
|
||||
[timeouts]
|
||||
server_timeout = "30s"
|
||||
reconnect_wait = "10s"
|
||||
close_timeout = "10s"
|
||||
ack_wait_timeout = "5s"
|
||||
|
||||
[[body]]
|
||||
name = "FPL"
|
||||
[[body.patterns]]
|
||||
pattern = "^\\((?P<type>[A-Z]{3})\\-(?P<number>[A-Z]+\\d+)\\-(?P<indicator>[A-Z]{2})(?:.*\\s*)?\\-(?P<aircraft>[A-Z]+\\d+/?[A-Z]?)\\s*\\-(?P<surve>.*)\\s*\\-(?P<departure>[A-Z]{4})(?P<departure_time>\\d{4})\\s*\\-(?P<speed>[A-Z]+\\d+)(?P<level>[A-Z0-9]+)\\s(?P<route>.*)\\s*\\-(?P<destination>[A-Z]{4})(?P<estt>\\d{4})\\s(?P<alter>[A-Z]{4})\\s*\\-(?P<pbn>PBN\\/[A-Z0-9]+)\\s(?P<nav>NAV\\/\\w+)\\sREG\\/(?P<reg>[A-Z0-9]+)\\sEET\\/(?P<eet>\\w{4}\\d{4})\\sSEL\\/(?P<sel>\\w+)\\sPER\\/(?P<performance>\\w)\\sRIF\\/(?P<rif>\\w+\\s[A-Z0-9]+\\s[A-Z]+)\\s*RMK\\/(?P<remark>.*)\\)$"
|
||||
comments = "FPL multi line expression"
|
||||
`
|
||||
os.Setenv("GO_ENV", "test")
|
||||
createTempConfigFile("test", content)
|
||||
defer os.RemoveAll("configs")
|
||||
|
||||
config, err := LoadConfig()
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to load valid config")
|
||||
Expect(config).NotTo(BeNil(), "config should not be nil")
|
||||
Expect(config.Nats.Client).To(Equal("test-client"), "nats.client should be 'test-client'")
|
||||
Expect(config.Subscription.Topic).To(Equal("example-topic"), "subscription.topic should be 'example-topic'")
|
||||
|
||||
// Validate the config
|
||||
err = ValidateConfig(config)
|
||||
Expect(err).NotTo(HaveOccurred(), "validation should pass for valid config")
|
||||
})
|
||||
})
|
||||
|
||||
Context("Loading and validating a non-existent file", func() {
|
||||
It("should return an error", func() {
|
||||
os.Setenv("GO_ENV", "nonexistent")
|
||||
defer os.RemoveAll("configs")
|
||||
|
||||
_, err := LoadConfig()
|
||||
Expect(err).To(HaveOccurred(), "expected error for non-existent config file")
|
||||
})
|
||||
})
|
||||
|
||||
Context("Loading and validating a file with invalid TOML format", func() {
|
||||
It("should return an error", func() {
|
||||
content := `
|
||||
invalid TOML content
|
||||
`
|
||||
os.Setenv("GO_ENV", "invalid")
|
||||
createTempConfigFile("invalid", content)
|
||||
defer os.RemoveAll("configs")
|
||||
|
||||
_, err := LoadConfig()
|
||||
Expect(err).To(HaveOccurred(), "expected error for invalid TOML format")
|
||||
})
|
||||
})
|
||||
|
||||
Context("Validating an invalid config structure", func() {
|
||||
It("should return an error for missing required fields", func() {
|
||||
invalidConfig := &Config{
|
||||
Nats: NatsConfig{
|
||||
Client: "",
|
||||
URL: "",
|
||||
},
|
||||
Subscription: SubscriptionConfig{
|
||||
Topic: "",
|
||||
},
|
||||
Body: []BodyConfig{},
|
||||
}
|
||||
|
||||
err := ValidateConfig(invalidConfig)
|
||||
Expect(err).To(HaveOccurred(), "expected validation error for invalid config")
|
||||
Expect(err.Error()).To(ContainSubstring("nats client is required"), "expected error for missing nats client")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"caatsm/pkg/utils"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AFTN 定义AFTN报文的结构
|
||||
type AFTN struct {
|
||||
Header Header `json:"header"` // 报文头部信息
|
||||
PriorityAndSender PriorityAndSender `json:"priority_and_sender"` // 优先级和发送地址信息
|
||||
TimeAndReceiver TimeAndReceiver `json:"time_and_receiver"` // 时间和接收地址信息
|
||||
Text string `json:"text"` // 报文内容
|
||||
ReceivedTime time.Time `json:"received_time"` // 收报时间,表示电报接收到的时间
|
||||
Category string `json:"category"`
|
||||
BodyData interface{} `json:"body_data"`
|
||||
}
|
||||
|
||||
// Header 定义AFTN报文的报头
|
||||
type Header struct {
|
||||
StartSignal string `json:"start_signal"` // 启动信号,表示报文的开始,通常为固定值
|
||||
SendID string `json:"send_id"` // 发送编号,用于唯一标识报文
|
||||
SendTime string `json:"send_time"` // 发送时间,格式为DDHHMM
|
||||
}
|
||||
|
||||
// 示例:
|
||||
// Header{
|
||||
// StartSignal: "ZCZC",
|
||||
// SendID: "TMQ2611",
|
||||
// SendTime: "151524",
|
||||
// }
|
||||
|
||||
// PriorityAndSender 定义优先级和发送地址
|
||||
type PriorityAndSender struct {
|
||||
Priority string `json:"priority"` // 优先级
|
||||
Sender string `json:"sender"` // 发报地址
|
||||
}
|
||||
|
||||
// 示例:
|
||||
// PriorityAndSender{
|
||||
// Priority: "FF",
|
||||
// Sender: "ZBTJZPZX",
|
||||
// }
|
||||
|
||||
// TimeAndReceiver 定义时间和接收地址
|
||||
type TimeAndReceiver struct {
|
||||
Time string `json:"time"` // 时间
|
||||
Receiver string `json:"receiver"` // 收报地址
|
||||
}
|
||||
|
||||
// 示例:
|
||||
// TimeAndReceiver{
|
||||
// Time: "151524",
|
||||
// Receiver: "ZGGGZPZX",
|
||||
// }
|
||||
|
||||
// Origin 定义AFTN报文的来源
|
||||
type Origin struct {
|
||||
OriginCode string `json:"origin_code"` // 发报地址代码
|
||||
FiledTime time.Time `json:"filed_time"` // 签发时间,表示电报生成的时间
|
||||
}
|
||||
|
||||
func (h *Header) Validate() error {
|
||||
// Validate SendTime format (e.g., DDHHMM)
|
||||
if len(h.SendTime) != 6 {
|
||||
err := "invalid send_time format"
|
||||
|
||||
utils.Logger.Error(err)
|
||||
return fmt.Errorf(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AFTN) Validate() error {
|
||||
if err := a.Header.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add more validation as needed
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("AFTN", func() {
|
||||
var original AFTN
|
||||
|
||||
BeforeEach(func() {
|
||||
original = AFTN{
|
||||
Header: Header{
|
||||
StartSignal: "ZCZC",
|
||||
SendID: "TMQ2611",
|
||||
SendTime: "151524",
|
||||
},
|
||||
PriorityAndSender: PriorityAndSender{
|
||||
Priority: "FF",
|
||||
Sender: "ZBTJZPZX",
|
||||
},
|
||||
TimeAndReceiver: TimeAndReceiver{
|
||||
Time: "151524",
|
||||
Receiver: "ZGGGZPZX",
|
||||
},
|
||||
Text: "Test message",
|
||||
ReceivedTime: time.Now(),
|
||||
Category: "Test",
|
||||
BodyData: nil,
|
||||
}
|
||||
})
|
||||
|
||||
Describe("Marshalling and Unmarshalling", func() {
|
||||
It("should marshal and unmarshal correctly", func() {
|
||||
data, err := json.Marshal(original)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var unmarshalled AFTN
|
||||
err = json.Unmarshal(data, &unmarshalled)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(unmarshalled.Header).To(Equal(original.Header))
|
||||
Expect(unmarshalled.PriorityAndSender).To(Equal(original.PriorityAndSender))
|
||||
Expect(unmarshalled.TimeAndReceiver).To(Equal(original.TimeAndReceiver))
|
||||
Expect(unmarshalled.Text).To(Equal(original.Text))
|
||||
Expect(unmarshalled.Category).To(Equal(original.Category))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
package domain
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
预警报文(ALN)通常包括以下内容:
|
||||
|
||||
编组 3:电报类别、编号和参考数据
|
||||
编组 7:航空器识别标志和 SSR 模式及编码
|
||||
编组 8:飞行规则和类型
|
||||
编组 13:起飞机场和时间
|
||||
编组 16:目的地机场和估计总耗时,目的地备降机场
|
||||
编组 18:其他信息(如需要)
|
||||
*/
|
||||
|
||||
/*
|
||||
MessageType (电报类别): Indicates the type of telegram (e.g., "ALN").
|
||||
AircraftID (航空器识别标志): Unique identifier of the aircraft.
|
||||
SSRModeAndCode (SSR 模式及编码): SSR (Secondary Surveillance Radar) mode and code.
|
||||
FlightRulesAndType (飞行规则和类型): Flight rules and type (e.g., IFR).
|
||||
DepartureAirport (起飞机场): ICAO code of the departure airport.
|
||||
DepartureTime (起飞时间): Departure time in UTC.
|
||||
ArrivalAirport (到达机场): ICAO code of the arrival airport.
|
||||
ArrivalTime (到达时间): Estimated arrival time in UTC.
|
||||
OtherInfo (其他信息): Optional field for any additional relevant information.
|
||||
|
||||
*/
|
||||
/*
|
||||
(ALN-CCA1234-IS
|
||||
-B6513
|
||||
-A1234
|
||||
-IFR
|
||||
-ZBTJ1200
|
||||
-ZGGG1335
|
||||
-ESTIMATED TIME EN ROUTE 01:35
|
||||
-Additional information)
|
||||
|
||||
*/
|
||||
|
||||
// ALN 电报体中的预警报文结构
|
||||
type ALN struct {
|
||||
Category string `json:"category"` // 电报类别
|
||||
AircraftID string `json:"aircraft_id"` // 航空器识别标志
|
||||
SSRModeAndCode string `json:"ssr_mode_and_code"` // SSR 模式及编码
|
||||
FlightRulesAndType string `json:"flight_rules_and_type"` // 飞行规则和类型
|
||||
DepartureAirport string `json:"departure_airport"` // 起飞机场
|
||||
DepartureTime string `json:"departure_time"` // 起飞时间
|
||||
ArrivalAirport string `json:"arrival_airport"` // 到达机场
|
||||
ArrivalTime string `json:"arrival_time"` // 到达时间
|
||||
OtherInfo string `json:"other_info,omitempty"` // 其他信息 (optional)
|
||||
}
|
||||
|
||||
// Validate validates the ALN struct fields
|
||||
func (a *ALN) Validate() error {
|
||||
if a.Category == "" {
|
||||
return fmt.Errorf("telegram category is required")
|
||||
}
|
||||
if a.AircraftID == "" {
|
||||
return fmt.Errorf("aircraft id is required")
|
||||
}
|
||||
if a.SSRModeAndCode == "" {
|
||||
return fmt.Errorf("ssr mode and code is required")
|
||||
}
|
||||
if a.FlightRulesAndType == "" {
|
||||
return fmt.Errorf("flight rules and type is required")
|
||||
}
|
||||
if a.DepartureAirport == "" {
|
||||
return fmt.Errorf("departure airport is required")
|
||||
}
|
||||
if a.DepartureTime == "" {
|
||||
return fmt.Errorf("departure time is required")
|
||||
}
|
||||
if a.ArrivalAirport == "" {
|
||||
return fmt.Errorf("arrival airport is required")
|
||||
}
|
||||
if a.ArrivalTime == "" {
|
||||
return fmt.Errorf("arrival time is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("ALN", func() {
|
||||
var original ALN
|
||||
|
||||
BeforeEach(func() {
|
||||
original = ALN{
|
||||
Category: "AFTN",
|
||||
AircraftID: "ABCD1234",
|
||||
SSRModeAndCode: "A1234",
|
||||
FlightRulesAndType: "IFR",
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: time.Now().Format("150405"), // HHMMSS format
|
||||
ArrivalAirport: "LAX",
|
||||
ArrivalTime: time.Now().Add(5 * time.Hour).Format("150405"), // HHMMSS format
|
||||
OtherInfo: "Test flight",
|
||||
}
|
||||
})
|
||||
|
||||
Describe("Marshalling and Unmarshalling", func() {
|
||||
It("should marshal and unmarshal correctly", func() {
|
||||
data, err := json.Marshal(original)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var unmarshalled ALN
|
||||
err = json.Unmarshal(data, &unmarshalled)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(unmarshalled).To(Equal(original))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Validation", func() {
|
||||
It("should validate successfully for a valid ALN", func() {
|
||||
err := original.Validate()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should fail validation for missing required fields", func() {
|
||||
invalidALN := ALN{
|
||||
Category: "AFTN",
|
||||
// AircraftID is missing
|
||||
SSRModeAndCode: "A1234",
|
||||
FlightRulesAndType: "IFR",
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: "150405",
|
||||
ArrivalAirport: "LAX",
|
||||
ArrivalTime: "180405",
|
||||
}
|
||||
|
||||
err := invalidALN.Validate()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("aircraft id is required"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
package domain
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
ARR 报文的规范和组成如下:
|
||||
|
||||
编组 3:电报类别、编号和参考数据
|
||||
编组 7:航空器识别标志和 SSR 模式及编码
|
||||
编组 13:起飞机场和时间
|
||||
编组 16:目的地机场和估计总耗时,目的地备降机场
|
||||
编组 18:其他信息(如需要)
|
||||
*/
|
||||
|
||||
/*
|
||||
TelegramCategory: Added for the telegram category (from Group 3).
|
||||
FlightNumber: Added for the flight number (from Group 3).
|
||||
ReferenceData: Added for the reference data (from Group 3).
|
||||
AircraftID: Kept as it was for the aircraft identification (from Group 7).
|
||||
SSRModeAndCode: Kept as it was for the SSR mode and code (from Group 7).
|
||||
DepartureAirport: Kept as it was for the departure airport (from Group 13).
|
||||
DepartureTime: Added for the departure time (from Group 13).
|
||||
ArrivalAirport: Kept as it was for the arrival airport (from Group 16).
|
||||
EstimatedElapsedTime: Added for the estimated total elapsed time (from Group 16).
|
||||
AlternateAirport: Added for the alternate destination airport (from Group 16).
|
||||
OtherInfo: Kept as it was for any other information (from Group 18).
|
||||
*/
|
||||
|
||||
// ARR 电报体中的到达报文结构
|
||||
type ARR struct {
|
||||
Category string `json:"category"` // 电报类别
|
||||
AircraftID string `json:"aircraft_id"` // 航空器识别标志
|
||||
SSRModeAndCode string `json:"ssr_mode_and_code"` // SSR 模式及编码(可选)
|
||||
DepartureAirport string `json:"departure_airport"` // 起飞机场
|
||||
DepartureTime string `json:"departure_time"` // 起飞时间
|
||||
ArrivalAirport string `json:"arrival_airport"` // 到达机场
|
||||
ArrivalTime string `json:"arrival_time"` // 到达时间
|
||||
EstimatedElapsedTime string `json:"estimated_elapsed_time"` // 估计总耗时(可选)
|
||||
AlternateAirport string `json:"alternate_airport"` // 目的地备降机场(可选)
|
||||
OtherInfo string `json:"other_info"` // 其他信息(可选)
|
||||
}
|
||||
|
||||
// Validate validates the ARR struct fields
|
||||
func (a *ARR) Validate() error {
|
||||
if a.Category == "" {
|
||||
return fmt.Errorf("category is required")
|
||||
}
|
||||
if a.AircraftID == "" {
|
||||
return fmt.Errorf("aircraft id is required")
|
||||
}
|
||||
if a.DepartureAirport == "" {
|
||||
return fmt.Errorf("departure airport is required")
|
||||
}
|
||||
if a.DepartureTime == "" {
|
||||
return fmt.Errorf("departure time is required")
|
||||
}
|
||||
if a.ArrivalAirport == "" {
|
||||
return fmt.Errorf("arrival airport is required")
|
||||
}
|
||||
if a.ArrivalTime == "" {
|
||||
return fmt.Errorf("arrival time is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("ARR", func() {
|
||||
var original ARR
|
||||
|
||||
BeforeEach(func() {
|
||||
original = ARR{
|
||||
Category: "ARR",
|
||||
AircraftID: "ABCD1234",
|
||||
SSRModeAndCode: "A1234",
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: time.Now().Format("150405"), // HHMMSS format
|
||||
ArrivalAirport: "LAX",
|
||||
ArrivalTime: time.Now().Add(5 * time.Hour).Format("150405"), // HHMMSS format
|
||||
OtherInfo: "Test flight",
|
||||
}
|
||||
})
|
||||
|
||||
Describe("Marshalling and Unmarshalling", func() {
|
||||
It("should marshal and unmarshal correctly", func() {
|
||||
data, err := json.Marshal(original)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var unmarshalled ARR
|
||||
err = json.Unmarshal(data, &unmarshalled)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(unmarshalled).To(Equal(original))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Validation", func() {
|
||||
It("should validate successfully for a valid ARR", func() {
|
||||
err := original.Validate()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should fail validation for missing required fields", func() {
|
||||
invalidARR := ARR{
|
||||
Category: "ARR",
|
||||
// AircraftID is missing
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: "150405",
|
||||
ArrivalAirport: "LAX",
|
||||
ArrivalTime: "180405",
|
||||
}
|
||||
|
||||
err := invalidARR.Validate()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("aircraft id is required"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
package domain
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
航班计划修改报文(CHG)一般包括以下内容:
|
||||
|
||||
编组 3:电报类别、编号和参考数据
|
||||
编组 7:航空器识别标志和 SSR 模式及编码
|
||||
编组 13:起飞机场和时间
|
||||
编组 16:目的地机场和估计总耗时,目的地备降机场
|
||||
编组 18:其他信息(如需要)
|
||||
编组 22:修改部分
|
||||
*/
|
||||
|
||||
/*
|
||||
Explanation of Each Field
|
||||
MessageType (电报类别):
|
||||
|
||||
Field: MessageType
|
||||
Description: Indicates the type of telegram (e.g., "CHG").
|
||||
AircraftID (航空器识别标志):
|
||||
|
||||
Field: AircraftID
|
||||
Description: Unique identifier of the aircraft.
|
||||
SSRModeAndCode (SSR 模式及编码):
|
||||
|
||||
Field: SSRModeAndCode
|
||||
Description: SSR (Secondary Surveillance Radar) mode and code.
|
||||
DepartureAirport (起飞机场):
|
||||
|
||||
Field: DepartureAirport
|
||||
Description: ICAO code of the departure airport.
|
||||
DepartureTime (起飞时间):
|
||||
|
||||
Field: DepartureTime
|
||||
Description: Departure time in UTC.
|
||||
ArrivalAirport (到达机场):
|
||||
|
||||
Field: ArrivalAirport
|
||||
Description: ICAO code of the arrival airport.
|
||||
ArrivalTime (到达时间):
|
||||
|
||||
Field: ArrivalTime
|
||||
Description: Estimated arrival time in UTC.
|
||||
OtherInfo (其他信息):
|
||||
|
||||
Field: OtherInfo
|
||||
Description: Optional field for any additional relevant information.
|
||||
ChangePart (修改部分):
|
||||
|
||||
Field: ChangePart
|
||||
Description: Indicates the part of the flight plan that is being changed.
|
||||
*/
|
||||
|
||||
/*
|
||||
(CHG-CCA5678-IS
|
||||
-B6513
|
||||
-A1234
|
||||
-ZBTJ1200
|
||||
-ZGGG1335
|
||||
-NEW ROUTE VIA PIAKS G330 PIMOL
|
||||
-Change reason or additional information)
|
||||
|
||||
*/
|
||||
|
||||
// CHG 电报体中的航班计划修改报文结构
|
||||
type CHG struct {
|
||||
Category string `json:"category"` // 电报类别
|
||||
AircraftID string `json:"aircraft_id"` // 航空器识别标志
|
||||
SSRModeAndCode string `json:"ssr_mode_and_code"` // SSR 模式及编码
|
||||
DepartureAirport string `json:"departure_airport"` // 起飞机场
|
||||
DepartureTime string `json:"departure_time"` // 起飞时间
|
||||
ArrivalAirport string `json:"arrival_airport"` // 到达机场
|
||||
ArrivalTime string `json:"arrival_time"` // 到达时间
|
||||
EstimatedElapsedTime string `json:"estimated_elapsed_time"` // 估计总耗时
|
||||
AlternateAirport string `json:"alternate_airport"` // 目的地备降机场 (optional)
|
||||
OtherInfo string `json:"other_info"` // 其他信息 (optional)
|
||||
ChangePart string `json:"change_part"` // 修改部分
|
||||
}
|
||||
|
||||
// Validate validates the CHG struct fields
|
||||
func (c *CHG) Validate() error {
|
||||
if c.Category == "" {
|
||||
return fmt.Errorf("category is required")
|
||||
}
|
||||
if c.AircraftID == "" {
|
||||
return fmt.Errorf("aircraft id is required")
|
||||
}
|
||||
if c.SSRModeAndCode == "" {
|
||||
return fmt.Errorf("ssr mode and code is required")
|
||||
}
|
||||
if c.DepartureAirport == "" {
|
||||
return fmt.Errorf("departure airport is required")
|
||||
}
|
||||
if c.DepartureTime == "" {
|
||||
return fmt.Errorf("departure time is required")
|
||||
}
|
||||
if c.ArrivalAirport == "" {
|
||||
return fmt.Errorf("arrival airport is required")
|
||||
}
|
||||
if c.ArrivalTime == "" {
|
||||
return fmt.Errorf("arrival time is required")
|
||||
}
|
||||
if c.EstimatedElapsedTime == "" {
|
||||
return fmt.Errorf("estimated elapsed time is required")
|
||||
}
|
||||
if c.ChangePart == "" {
|
||||
return fmt.Errorf("change part is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("CHG", func() {
|
||||
var original CHG
|
||||
|
||||
BeforeEach(func() {
|
||||
original = CHG{
|
||||
Category: "CHG",
|
||||
AircraftID: "ABCD1234",
|
||||
SSRModeAndCode: "A1234",
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: time.Now().Format("150405"), // HHMMSS format
|
||||
ArrivalAirport: "LAX",
|
||||
ArrivalTime: time.Now().Add(5 * time.Hour).Format("150405"), // HHMMSS format
|
||||
EstimatedElapsedTime: "0500", // Example time format
|
||||
ChangePart: "Flight plan",
|
||||
OtherInfo: "Test flight",
|
||||
}
|
||||
})
|
||||
|
||||
Describe("Marshalling and Unmarshalling", func() {
|
||||
It("should marshal and unmarshal correctly", func() {
|
||||
data, err := json.Marshal(original)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var unmarshalled CHG
|
||||
err = json.Unmarshal(data, &unmarshalled)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(unmarshalled).To(Equal(original))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Validation", func() {
|
||||
It("should validate successfully for a valid CHG", func() {
|
||||
err := original.Validate()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should fail validation for missing required fields", func() {
|
||||
invalidCHG := CHG{
|
||||
Category: "CHG",
|
||||
// AircraftID is missing
|
||||
SSRModeAndCode: "A1234",
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: "150405",
|
||||
ArrivalAirport: "LAX",
|
||||
ArrivalTime: "180405",
|
||||
EstimatedElapsedTime: "0500",
|
||||
ChangePart: "Flight plan",
|
||||
}
|
||||
|
||||
err := invalidCHG.Validate()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("aircraft id is required"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,129 @@
|
||||
package domain
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
航班计划变更报文(CPL)通常包括以下内容:
|
||||
|
||||
编组 3:电报类别、编号和参考数据
|
||||
编组 7:航空器识别标志和 SSR 模式及编码
|
||||
编组 8:飞行规则和类型
|
||||
编组 9:航机和设备
|
||||
编组 10:巡航速度和飞行高度
|
||||
编组 13:起飞机场和时间
|
||||
编组 15:航路
|
||||
编组 16:目的地机场和总时间,目的地备降机场
|
||||
编组 18:其他信息(如需要)
|
||||
*/
|
||||
/*
|
||||
MessageType (电报类别):
|
||||
|
||||
Field: MessageType
|
||||
Description: Indicates the type of telegram (e.g., "CPL").
|
||||
AircraftID (航空器识别标志):
|
||||
|
||||
Field: AircraftID
|
||||
Description: Unique identifier of the aircraft.
|
||||
SSRModeAndCode (SSR 模式及编码):
|
||||
|
||||
Field: SSRModeAndCode
|
||||
Description: SSR (Secondary Surveillance Radar) mode and code.
|
||||
FlightRulesAndType (飞行规则和类型):
|
||||
|
||||
Field: FlightRulesAndType
|
||||
Description: Flight rules and type (e.g., IFR).
|
||||
AircraftAndEquipment (航机和设备):
|
||||
|
||||
Field: AircraftAndEquipment
|
||||
Description: Aircraft type and equipment.
|
||||
CruisingSpeedAndLevel (巡航速度和飞行高度):
|
||||
|
||||
Field: CruisingSpeedAndLevel
|
||||
Description: Cruising speed and flight level.
|
||||
DepartureAirport (起飞机场):
|
||||
|
||||
Field: DepartureAirport
|
||||
Description: ICAO code of the departure airport.
|
||||
DepartureTime (起飞时间):
|
||||
|
||||
Field: DepartureTime
|
||||
Description: Departure time in UTC.
|
||||
Route (航路):
|
||||
|
||||
Field: Route
|
||||
Description: Planned route.
|
||||
DestinationAndTotalTime (目的地机场和总时间):
|
||||
|
||||
Field: DestinationAndTotalTime
|
||||
Description: ICAO code of the destination airport and total elapsed time.
|
||||
OtherInfo (其他信息):
|
||||
|
||||
Field: OtherInfo
|
||||
Description: Optional field for any additional relevant information.
|
||||
*/
|
||||
|
||||
/*
|
||||
(CPL-CCA7890-IS
|
||||
-B6513
|
||||
-A1234
|
||||
-IFR
|
||||
-A332/H
|
||||
-K0859S1040
|
||||
-ZBTJ1200
|
||||
-PIAKS G330 PIMOL A539 BTO W82 DOGAR
|
||||
-ZGGG0135
|
||||
-ZBAA
|
||||
-Additional information)
|
||||
|
||||
*/
|
||||
|
||||
// CPL 电报体中的航班计划变更报文结构
|
||||
type CPL struct {
|
||||
Category string `json:"category"` // 电报类别
|
||||
AircraftID string `json:"aircraft_id"` // 航空器识别标志
|
||||
SSRModeAndCode string `json:"ssr_mode_and_code"` // SSR 模式及编码
|
||||
FlightRulesAndType string `json:"flight_rules_and_type"` // 飞行规则和类型
|
||||
AircraftAndEquipment string `json:"aircraft_and_equipment"` // 航机和设备
|
||||
CruisingSpeedAndLevel string `json:"cruising_speed_and_level"` // 巡航速度和飞行高度
|
||||
DepartureAirport string `json:"departure_airport"` // 起飞机场
|
||||
DepartureTime string `json:"departure_time"` // 起飞时间
|
||||
Route string `json:"route"` // 航路
|
||||
DestinationAndTotalTime string `json:"destination_and_total_time"` // 目的地机场和总时间
|
||||
AlternateAirport string `json:"alternate_airport,omitempty"` // 目的地备降机场 (optional)
|
||||
OtherInfo string `json:"other_info,omitempty"` // 其他信息 (optional)
|
||||
}
|
||||
|
||||
// Validate validates the CPL struct fields
|
||||
func (c *CPL) Validate() error {
|
||||
if c.Category == "" {
|
||||
return fmt.Errorf("category is required")
|
||||
}
|
||||
if c.AircraftID == "" {
|
||||
return fmt.Errorf("aircraft id is required")
|
||||
}
|
||||
if c.SSRModeAndCode == "" {
|
||||
return fmt.Errorf("ssr mode and code is required")
|
||||
}
|
||||
if c.FlightRulesAndType == "" {
|
||||
return fmt.Errorf("flight rules and type is required")
|
||||
}
|
||||
if c.AircraftAndEquipment == "" {
|
||||
return fmt.Errorf("aircraft and equipment is required")
|
||||
}
|
||||
if c.CruisingSpeedAndLevel == "" {
|
||||
return fmt.Errorf("cruising speed and level is required")
|
||||
}
|
||||
if c.DepartureAirport == "" {
|
||||
return fmt.Errorf("departure airport is required")
|
||||
}
|
||||
if c.DepartureTime == "" {
|
||||
return fmt.Errorf("departure time is required")
|
||||
}
|
||||
if c.Route == "" {
|
||||
return fmt.Errorf("route is required")
|
||||
}
|
||||
if c.DestinationAndTotalTime == "" {
|
||||
return fmt.Errorf("destination and total time is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("CPL", func() {
|
||||
var original CPL
|
||||
|
||||
BeforeEach(func() {
|
||||
original = CPL{
|
||||
Category: "CPL",
|
||||
AircraftID: "ABCD1234",
|
||||
SSRModeAndCode: "A1234",
|
||||
FlightRulesAndType: "IFR",
|
||||
AircraftAndEquipment: "B738",
|
||||
CruisingSpeedAndLevel: "N0450F350",
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: time.Now().Format("150405"), // HHMMSS format
|
||||
Route: "DCT GAYEL J95 BUF DCT",
|
||||
DestinationAndTotalTime: "LAX0500", // Example format
|
||||
OtherInfo: "Test flight",
|
||||
}
|
||||
})
|
||||
|
||||
Describe("Marshalling and Unmarshalling", func() {
|
||||
It("should marshal and unmarshal correctly", func() {
|
||||
data, err := json.Marshal(original)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var unmarshalled CPL
|
||||
err = json.Unmarshal(data, &unmarshalled)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(unmarshalled).To(Equal(original))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Validation", func() {
|
||||
It("should validate successfully for a valid CPL", func() {
|
||||
err := original.Validate()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should fail validation for missing required fields", func() {
|
||||
invalidCPL := CPL{
|
||||
Category: "CPL",
|
||||
// AircraftID is missing
|
||||
SSRModeAndCode: "A1234",
|
||||
FlightRulesAndType: "IFR",
|
||||
AircraftAndEquipment: "B738",
|
||||
CruisingSpeedAndLevel: "N0450F350",
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: "150405",
|
||||
Route: "DCT GAYEL J95 BUF DCT",
|
||||
DestinationAndTotalTime: "LAX0500",
|
||||
}
|
||||
|
||||
err := invalidCPL.Validate()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("aircraft id is required"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
package domain
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
起飞报(DEP)报文的规范和组成如下:
|
||||
|
||||
编组 3:电报类别、编号和参考数据
|
||||
编组 7:航空器识别标志和 SSR 模式及编码
|
||||
编组 13:起飞机场和时间
|
||||
编组 16:目的地机场和估计总耗时,目的地备降机场
|
||||
编组 18:其他信息(如需要)
|
||||
*/
|
||||
|
||||
/*
|
||||
TelegramCategory (电报类别):
|
||||
|
||||
Field: TelegramCategory
|
||||
Description: This field indicates the type of telegram, which in this case would be "DEP" for departure.
|
||||
AircraftID (航空器识别标志):
|
||||
|
||||
Field: AircraftID
|
||||
Description: This field contains the unique identifier of the aircraft.
|
||||
SSRModeAndCode (SSR 模式及编码, optional):
|
||||
|
||||
Field: SSRModeAndCode
|
||||
Description: This field contains the SSR (Secondary Surveillance Radar) mode and code. It is optional and indicated as a pointer.
|
||||
DepartureAirport (起飞机场):
|
||||
|
||||
Field: DepartureAirport
|
||||
Description: This field contains the ICAO code of the airport from which the aircraft is departing.
|
||||
DepartureTime (起飞时间):
|
||||
|
||||
Field: DepartureTime
|
||||
Description: This field contains the departure time in UTC.
|
||||
Destination (目的地机场):
|
||||
|
||||
Field: Destination
|
||||
Description: This field contains the ICAO code of the destination airport.
|
||||
EstimatedElapsedTime (估计总耗时):
|
||||
|
||||
Field: EstimatedElapsedTime
|
||||
Description: This field contains the estimated total elapsed time of the flight.
|
||||
AlternateAirport (目的地备降机场, optional):
|
||||
|
||||
Field: AlternateAirport
|
||||
Description: This field contains the ICAO code of the alternate destination airport. It is optional and indicated as a pointer.
|
||||
OtherInfo (其他信息, optional):
|
||||
|
||||
Field: OtherInfo
|
||||
Description: This field contains any additional relevant information. It is optional and indicated as a pointer.
|
||||
*/
|
||||
// DEP 电报体中的起飞报文结构
|
||||
type DEP struct {
|
||||
TelegramCategory string `json:"telegram_category"` // 电报类别
|
||||
AircraftID string `json:"aircraft_id"` // 航空器识别标志
|
||||
SSRModeAndCode string `json:"ssr_mode_and_code,omitempty"` // SSR 模式及编码(可选)
|
||||
DepartureAirport string `json:"departure_airport"` // 起飞机场
|
||||
DepartureTime string `json:"departure_time"` // 起飞时间
|
||||
Destination string `json:"destination"` // 目的地机场
|
||||
EstimatedElapsedTime string `json:"estimated_elapsed_time"` // 估计总耗时
|
||||
AlternateAirport string `json:"alternate_airport,omitempty"` // 目的地备降机场(可选)
|
||||
OtherInfo string `json:"other_info,omitempty"` // 其他信息(可选)
|
||||
}
|
||||
|
||||
// Validate validates the DEP struct fields
|
||||
func (d *DEP) Validate() error {
|
||||
if d.TelegramCategory == "" {
|
||||
return fmt.Errorf("telegram category is required")
|
||||
}
|
||||
if d.AircraftID == "" {
|
||||
return fmt.Errorf("aircraft id is required")
|
||||
}
|
||||
if d.DepartureAirport == "" {
|
||||
return fmt.Errorf("departure airport is required")
|
||||
}
|
||||
if d.DepartureTime == "" {
|
||||
return fmt.Errorf("departure time is required")
|
||||
}
|
||||
if d.Destination == "" {
|
||||
return fmt.Errorf("destination is required")
|
||||
}
|
||||
if d.EstimatedElapsedTime == "" {
|
||||
return fmt.Errorf("estimated elapsed time is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("DEP", func() {
|
||||
var original DEP
|
||||
|
||||
BeforeEach(func() {
|
||||
original = DEP{
|
||||
TelegramCategory: "DEP",
|
||||
AircraftID: "ABCD1234",
|
||||
SSRModeAndCode: "A1234",
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: time.Now().Format("150405"), // HHMMSS format
|
||||
Destination: "LAX",
|
||||
EstimatedElapsedTime: "0500", // Example format
|
||||
OtherInfo: "Test flight",
|
||||
}
|
||||
})
|
||||
|
||||
Describe("Marshalling and Unmarshalling", func() {
|
||||
It("should marshal and unmarshal correctly", func() {
|
||||
data, err := json.Marshal(original)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var unmarshalled DEP
|
||||
err = json.Unmarshal(data, &unmarshalled)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(unmarshalled).To(Equal(original))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Validation", func() {
|
||||
It("should validate successfully for a valid DEP", func() {
|
||||
err := original.Validate()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should fail validation for missing required fields", func() {
|
||||
invalidDEP := DEP{
|
||||
TelegramCategory: "DEP",
|
||||
// AircraftID is missing
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: "150405",
|
||||
Destination: "LAX",
|
||||
EstimatedElapsedTime: "0500",
|
||||
}
|
||||
|
||||
err := invalidDEP.Validate()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("aircraft id is required"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
package domain
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
延误报文(DLA)通常包括以下内容:
|
||||
|
||||
编组 3:电报类别、编号和参考数据
|
||||
编组 7:航空器识别标志和 SSR 模式及编码
|
||||
编组 13:起飞机场和新的起飞时间
|
||||
编组 16:目的地机场和估计总耗时
|
||||
编组 18:其他信息(如需要)
|
||||
*/
|
||||
/*
|
||||
MessageType (电报类别):
|
||||
|
||||
Field: MessageType
|
||||
Description: Indicates the type of telegram (e.g., "DLA").
|
||||
AircraftID (航空器识别标志):
|
||||
|
||||
Field: AircraftID
|
||||
Description: Unique identifier of the aircraft.
|
||||
SSRModeAndCode (SSR 模式及编码):
|
||||
|
||||
Field: SSRModeAndCode
|
||||
Description: SSR (Secondary Surveillance Radar) mode and code.
|
||||
DepartureAirport (起飞机场):
|
||||
|
||||
Field: DepartureAirport
|
||||
Description: ICAO code of the departure airport.
|
||||
NewDepartureTime (新的起飞时间):
|
||||
|
||||
Field: NewDepartureTime
|
||||
Description: New departure time in UTC.
|
||||
ArrivalAirport (到达机场):
|
||||
|
||||
Field: ArrivalAirport
|
||||
Description: ICAO code of the arrival airport.
|
||||
EstimatedElapsedTime (估计总耗时):
|
||||
|
||||
Field: EstimatedElapsedTime
|
||||
Description: Estimated total elapsed time.
|
||||
OtherInfo (其他信息):
|
||||
|
||||
Field: OtherInfo
|
||||
Description: Optional field for any additional relevant information.
|
||||
*/
|
||||
/*
|
||||
(DLA-CCA7890-IS
|
||||
-B6513
|
||||
-A1234
|
||||
-ZBTJ1500
|
||||
-ZGGG0135
|
||||
-Weather delay)
|
||||
*/
|
||||
|
||||
// DLA 电报体中的延误报文结构
|
||||
type DLA struct {
|
||||
Category string `json:"category"` // 电报类别
|
||||
AircraftID string `json:"aircraft_id"` // 航空器识别标志
|
||||
SSRModeAndCode string `json:"ssr_mode_and_code"` // SSR 模式及编码
|
||||
DepartureAirport string `json:"departure_airport"` // 起飞机场
|
||||
NewDepartureTime string `json:"new_departure_time"` // 新的起飞时间
|
||||
ArrivalAirport string `json:"arrival_airport"` // 到达机场
|
||||
EstimatedElapsedTime string `json:"estimated_elapsed_time"` // 估计总耗时
|
||||
OtherInfo string `json:"other_info,omitempty"` // 其他信息 (optional)
|
||||
}
|
||||
|
||||
// Validate validates the DLA struct fields
|
||||
func (d *DLA) Validate() error {
|
||||
if d.Category == "" {
|
||||
return fmt.Errorf("telegram category is required")
|
||||
}
|
||||
if d.AircraftID == "" {
|
||||
return fmt.Errorf("aircraft id is required")
|
||||
}
|
||||
if d.SSRModeAndCode == "" {
|
||||
return fmt.Errorf("ssr mode and code is required")
|
||||
}
|
||||
if d.DepartureAirport == "" {
|
||||
return fmt.Errorf("departure airport is required")
|
||||
}
|
||||
if d.NewDepartureTime == "" {
|
||||
return fmt.Errorf("new departure time is required")
|
||||
}
|
||||
if d.ArrivalAirport == "" {
|
||||
return fmt.Errorf("arrival airport is required")
|
||||
}
|
||||
if d.EstimatedElapsedTime == "" {
|
||||
return fmt.Errorf("estimated elapsed time is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("DLA", func() {
|
||||
var original DLA
|
||||
|
||||
BeforeEach(func() {
|
||||
original = DLA{
|
||||
Category: "DLA",
|
||||
AircraftID: "ABCD1234",
|
||||
SSRModeAndCode: "A1234",
|
||||
DepartureAirport: "JFK",
|
||||
NewDepartureTime: time.Now().Add(1 * time.Hour).Format("150405"), // HHMMSS format
|
||||
ArrivalAirport: "LAX",
|
||||
EstimatedElapsedTime: "0500", // Example format
|
||||
OtherInfo: "Test flight delay",
|
||||
}
|
||||
})
|
||||
|
||||
Describe("Marshalling and Unmarshalling", func() {
|
||||
It("should marshal and unmarshal correctly", func() {
|
||||
data, err := json.Marshal(original)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var unmarshalled DLA
|
||||
err = json.Unmarshal(data, &unmarshalled)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(unmarshalled).To(Equal(original))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Validation", func() {
|
||||
It("should validate successfully for a valid DLA", func() {
|
||||
err := original.Validate()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should fail validation for missing required fields", func() {
|
||||
invalidDLA := DLA{
|
||||
Category: "DLA",
|
||||
// AircraftID is missing
|
||||
SSRModeAndCode: "A1234",
|
||||
DepartureAirport: "JFK",
|
||||
NewDepartureTime: "150405",
|
||||
ArrivalAirport: "LAX",
|
||||
EstimatedElapsedTime: "0500",
|
||||
}
|
||||
|
||||
err := invalidDLA.Validate()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("aircraft id is required"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
package domain
|
||||
|
||||
import "fmt"
|
||||
|
||||
/*
|
||||
飞行计划报文(FPL)通常包括以下内容:
|
||||
|
||||
编组 3:电报类别、编号和参考数据
|
||||
编组 7:航空器识别标志和 SSR 模式及编码
|
||||
编组 8:飞行规则和类型
|
||||
编组 9:航机和设备
|
||||
编组 10:巡航速度和飞行高度
|
||||
编组 13:起飞机场和时间
|
||||
编组 15:航路
|
||||
编组 16:目的地机场和估计总耗时
|
||||
编组 18:其他信息(如需要)
|
||||
编组 19:补充信息
|
||||
*/
|
||||
/*
|
||||
Group 3: Telegram category, number, and reference data
|
||||
Group 7: Aircraft identification and SSR mode and code
|
||||
Group 8: Flight rules and type
|
||||
Group 9: Number of aircraft, type of aircraft, and wake turbulence category
|
||||
Group 10: Equipment and capabilities
|
||||
Group 13: Departure airport and time
|
||||
Group 15: Route
|
||||
Group 16: Destination airport and estimated total elapsed time, alternate destination airport
|
||||
Group 18: Other information (if needed)
|
||||
Group 19: Supplementary information (if needed)
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
MessageType (电报类别): Indicates the type of telegram.
|
||||
FlightNumber (航班号): Represents the flight number.
|
||||
ReferenceData (参考数据): Optional field for reference data.
|
||||
AircraftID (航空器识别标志): Unique identifier of the aircraft.
|
||||
SSRModeAndCode (SSR 模式及编码): SSR (Secondary Surveillance Radar) mode and code.
|
||||
FlightRulesAndType (飞行规则和类型): Flight rules and type (e.g., IFR).
|
||||
AircraftAndEquipment (航机和设备): Aircraft type and equipment.
|
||||
CruisingSpeedAndLevel (巡航速度和飞行高度): Cruising speed and flight level.
|
||||
DepartureAirport (起飞机场): ICAO code of the departure airport.
|
||||
DepartureTime (起飞时间): Departure time in UTC.
|
||||
Route (航路): Planned route.
|
||||
DestinationAndTotalTime (目的地机场和估计总耗时): ICAO code of the destination airport and estimated total flight time.
|
||||
AlternateAirport (目的地备降机场): Optional field for the alternate destination airport.
|
||||
OtherInfo (其他信息): Optional field for additional relevant information.
|
||||
SupplementaryInfo (补充信息): Optional field for supplementary information.
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
(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)
|
||||
|
||||
*/
|
||||
|
||||
// FPL 电报体中的飞行计划报文结构
|
||||
type FPL struct {
|
||||
Category string `json:"category"` // 电报类别
|
||||
FlightNumber string `json:"flight_number"` // 航班号
|
||||
ReferenceData string `json:"reference_data,omitempty"` // 参考数据(可选)
|
||||
AircraftID string `json:"aircraft_id"` // 航空器识别标志
|
||||
SSRModeAndCode string `json:"ssr_mode_and_code"` // SSR 模式及编码
|
||||
FlightRulesAndType string `json:"flight_rules_and_type"` // 飞行规则和类型
|
||||
AircraftAndEquipment string `json:"aircraft_and_equipment"` // 航机和设备
|
||||
CruisingSpeedAndLevel string `json:"cruising_speed_and_level"` // 巡航速度和飞行高度
|
||||
DepartureAirport string `json:"departure_airport"` // 起飞机场
|
||||
DepartureTime string `json:"departure_time"` // 起飞时间
|
||||
Route string `json:"route"` // 航路
|
||||
DestinationAndTotalTime string `json:"destination_and_total_time"` // 目的地机场和估计总耗时
|
||||
AlternateAirport string `json:"alternate_airport,omitempty"` // 目的地备降机场(可选)
|
||||
OtherInfo string `json:"other_info,omitempty"` // 其他信息(可选)
|
||||
SupplementaryInfo string `json:"supplementary_info,omitempty"` // 补充信息(可选)
|
||||
}
|
||||
|
||||
// Validate validates the FPL struct fields
|
||||
func (f *FPL) Validate() error {
|
||||
if f.Category == "" {
|
||||
return fmt.Errorf("telegram category is required")
|
||||
}
|
||||
if f.FlightNumber == "" {
|
||||
return fmt.Errorf("flight number is required")
|
||||
}
|
||||
if f.AircraftID == "" {
|
||||
return fmt.Errorf("aircraft id is required")
|
||||
}
|
||||
if f.SSRModeAndCode == "" {
|
||||
return fmt.Errorf("ssr mode and code is required")
|
||||
}
|
||||
if f.FlightRulesAndType == "" {
|
||||
return fmt.Errorf("flight rules and type is required")
|
||||
}
|
||||
if f.AircraftAndEquipment == "" {
|
||||
return fmt.Errorf("aircraft and equipment is required")
|
||||
}
|
||||
if f.CruisingSpeedAndLevel == "" {
|
||||
return fmt.Errorf("cruising speed and level is required")
|
||||
}
|
||||
if f.DepartureAirport == "" {
|
||||
return fmt.Errorf("departure airport is required")
|
||||
}
|
||||
if f.DepartureTime == "" {
|
||||
return fmt.Errorf("departure time is required")
|
||||
}
|
||||
if f.Route == "" {
|
||||
return fmt.Errorf("route is required")
|
||||
}
|
||||
if f.DestinationAndTotalTime == "" {
|
||||
return fmt.Errorf("destination and total time is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("FPL", func() {
|
||||
var original FPL
|
||||
|
||||
BeforeEach(func() {
|
||||
original = FPL{
|
||||
Category: "FPL",
|
||||
FlightNumber: "AB123",
|
||||
AircraftID: "ABCD1234",
|
||||
SSRModeAndCode: "A1234",
|
||||
FlightRulesAndType: "IFR",
|
||||
AircraftAndEquipment: "B738",
|
||||
CruisingSpeedAndLevel: "N0450F350",
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: time.Now().Format("150405"), // HHMMSS format
|
||||
Route: "DCT GAYEL J95 BUF DCT",
|
||||
DestinationAndTotalTime: "LAX0500", // Example format
|
||||
OtherInfo: "Test flight",
|
||||
SupplementaryInfo: "Supplementary information",
|
||||
}
|
||||
})
|
||||
|
||||
Describe("Marshalling and Unmarshalling", func() {
|
||||
It("should marshal and unmarshal correctly", func() {
|
||||
data, err := json.Marshal(original)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var unmarshalled FPL
|
||||
err = json.Unmarshal(data, &unmarshalled)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(unmarshalled).To(Equal(original))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Validation", func() {
|
||||
It("should validate successfully for a valid FPL", func() {
|
||||
err := original.Validate()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should fail validation for missing required fields", func() {
|
||||
invalidFPL := FPL{
|
||||
Category: "FPL",
|
||||
// FlightNumber is missing
|
||||
AircraftID: "ABCD1234",
|
||||
SSRModeAndCode: "A1234",
|
||||
FlightRulesAndType: "IFR",
|
||||
AircraftAndEquipment: "B738",
|
||||
CruisingSpeedAndLevel: "N0450F350",
|
||||
DepartureAirport: "JFK",
|
||||
DepartureTime: "150405",
|
||||
Route: "DCT GAYEL J95 BUF DCT",
|
||||
DestinationAndTotalTime: "LAX0500",
|
||||
}
|
||||
|
||||
err := invalidFPL.Validate()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("flight number is required"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestDomain(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Domain Suite")
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package nats
|
||||
|
||||
func Subscribe() {
|
||||
// Placeholder for the NATS subscription logic.
|
||||
}
|
||||
@@ -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 == ""
|
||||
}
|
||||
@@ -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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user