This commit is contained in:
windyboy
2024-07-19 20:02:18 +08:00
parent d1d427057e
commit dbbf6133be
26 changed files with 2171 additions and 0 deletions
+84
View File
@@ -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"
+26
View File
@@ -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")
}
+164
View File
@@ -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
}
+128
View File
@@ -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")
})
})
})
+81
View File
@@ -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
}
+52
View File
@@ -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))
})
})
})
+80
View File
@@ -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
}
+64
View File
@@ -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"))
})
})
})
+64
View File
@@ -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
}
+61
View File
@@ -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"))
})
})
})
+112
View File
@@ -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
}
+66
View File
@@ -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"))
})
})
})
+129
View File
@@ -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
}
+68
View File
@@ -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"))
})
})
})
+87
View File
@@ -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
}
+61
View File
@@ -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"))
})
})
})
+93
View File
@@ -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
}
+62
View File
@@ -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"))
})
})
})
+118
View File
@@ -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
}
+71
View File
@@ -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"))
})
})
})
+13
View File
@@ -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")
}
+5
View File
@@ -0,0 +1,5 @@
package nats
func Subscribe() {
// Placeholder for the NATS subscription logic.
}
+183
View File
@@ -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 == ""
}
+194
View File
@@ -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"))
})
})
})
+39
View File
@@ -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
}
+66
View File
@@ -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())
})
})
})