[feat](trx-configurator): add --check flag for config validation
Validates existing TOML config files for syntax correctness, unknown keys, and structural issues. Auto-detects config type (server, client, combined) and checks known sections against expected schema. Validates: log levels, coordinate ranges, port ranges, access types, lat/lon pairing, and unknown key warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -15,3 +15,6 @@ path = "src/main.rs"
|
|||||||
clap = { workspace = true, features = ["derive"] }
|
clap = { workspace = true, features = ["derive"] }
|
||||||
dialoguer = "0.11"
|
dialoguer = "0.11"
|
||||||
toml_edit = "0.22"
|
toml_edit = "0.22"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|||||||
@@ -0,0 +1,512 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use toml_edit::DocumentMut;
|
||||||
|
|
||||||
|
/// Known top-level keys for a standalone server config.
|
||||||
|
const SERVER_KEYS: &[&str] = &[
|
||||||
|
"general",
|
||||||
|
"rig",
|
||||||
|
"rigs",
|
||||||
|
"behavior",
|
||||||
|
"listen",
|
||||||
|
"audio",
|
||||||
|
"sdr",
|
||||||
|
"pskreporter",
|
||||||
|
"aprsfi",
|
||||||
|
"decode_logs",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Known top-level keys for a standalone client config.
|
||||||
|
const CLIENT_KEYS: &[&str] = &["general", "remote", "remotes", "frontends"];
|
||||||
|
|
||||||
|
/// Known top-level keys for a combined trx-rs.toml.
|
||||||
|
const COMBINED_KEYS: &[&str] = &["trx-server", "trx-client"];
|
||||||
|
|
||||||
|
/// Known sub-keys within [general] (server).
|
||||||
|
const SERVER_GENERAL_KEYS: &[&str] = &["callsign", "log_level", "latitude", "longitude"];
|
||||||
|
|
||||||
|
/// Known sub-keys within [general] (client).
|
||||||
|
const CLIENT_GENERAL_KEYS: &[&str] = &[
|
||||||
|
"callsign",
|
||||||
|
"log_level",
|
||||||
|
"website_url",
|
||||||
|
"website_name",
|
||||||
|
"ais_vessel_url_base",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Known sub-keys within [rig].
|
||||||
|
const RIG_KEYS: &[&str] = &["model", "initial_freq_hz", "initial_mode", "access"];
|
||||||
|
|
||||||
|
/// Known sub-keys within [rig.access].
|
||||||
|
const ACCESS_KEYS: &[&str] = &["type", "port", "baud", "host", "tcp_port", "args"];
|
||||||
|
|
||||||
|
/// Known sub-keys within [listen].
|
||||||
|
const LISTEN_KEYS: &[&str] = &["enabled", "listen", "port", "auth"];
|
||||||
|
|
||||||
|
/// Known sub-keys within [audio] (server).
|
||||||
|
const AUDIO_KEYS: &[&str] = &[
|
||||||
|
"enabled",
|
||||||
|
"listen",
|
||||||
|
"port",
|
||||||
|
"rx_enabled",
|
||||||
|
"tx_enabled",
|
||||||
|
"device",
|
||||||
|
"sample_rate",
|
||||||
|
"channels",
|
||||||
|
"frame_duration_ms",
|
||||||
|
"bitrate_bps",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Known sub-keys within [behavior].
|
||||||
|
const BEHAVIOR_KEYS: &[&str] = &[
|
||||||
|
"poll_interval_ms",
|
||||||
|
"poll_interval_tx_ms",
|
||||||
|
"max_retries",
|
||||||
|
"retry_base_delay_ms",
|
||||||
|
"vfo_prime",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Known sub-keys within [remote].
|
||||||
|
const REMOTE_KEYS: &[&str] = &["url", "rig_id", "auth", "poll_interval_ms"];
|
||||||
|
|
||||||
|
/// Known sub-keys within [frontends].
|
||||||
|
const FRONTENDS_KEYS: &[&str] = &["http", "rigctl", "http_json", "audio"];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum DetectedType {
|
||||||
|
Server,
|
||||||
|
Client,
|
||||||
|
Combined,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DetectedType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Server => write!(f, "server"),
|
||||||
|
Self::Client => write!(f, "client"),
|
||||||
|
Self::Combined => write!(f, "combined"),
|
||||||
|
Self::Unknown => write!(f, "unknown"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_file(path: &Path) -> Result<String, String> {
|
||||||
|
let content = std::fs::read_to_string(path)
|
||||||
|
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
|
||||||
|
|
||||||
|
// Step 1: TOML syntax check
|
||||||
|
let doc: DocumentMut = content
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("{}: TOML syntax error: {}", path.display(), e))?;
|
||||||
|
|
||||||
|
let mut report = String::new();
|
||||||
|
let mut warnings: Vec<String> = Vec::new();
|
||||||
|
let mut errors: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
let table = doc.as_table();
|
||||||
|
|
||||||
|
// Step 2: Detect config type
|
||||||
|
let detected = detect_type(table);
|
||||||
|
writeln!(report, "{}: valid TOML", path.display()).unwrap();
|
||||||
|
writeln!(report, " Detected type: {}", detected).unwrap();
|
||||||
|
|
||||||
|
// Step 3: Structural validation
|
||||||
|
match detected {
|
||||||
|
DetectedType::Server => {
|
||||||
|
check_unknown_keys(table, SERVER_KEYS, "", &mut warnings);
|
||||||
|
check_server_sections(table, "", &mut warnings, &mut errors);
|
||||||
|
}
|
||||||
|
DetectedType::Client => {
|
||||||
|
check_unknown_keys(table, CLIENT_KEYS, "", &mut warnings);
|
||||||
|
check_client_sections(table, "", &mut warnings, &mut errors);
|
||||||
|
}
|
||||||
|
DetectedType::Combined => {
|
||||||
|
check_unknown_keys(table, COMBINED_KEYS, "", &mut warnings);
|
||||||
|
if let Some(server) = table.get("trx-server").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(server, SERVER_KEYS, "[trx-server].", &mut warnings);
|
||||||
|
check_server_sections(server, "[trx-server].", &mut warnings, &mut errors);
|
||||||
|
}
|
||||||
|
if let Some(client) = table.get("trx-client").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(client, CLIENT_KEYS, "[trx-client].", &mut warnings);
|
||||||
|
check_client_sections(client, "[trx-client].", &mut warnings, &mut errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DetectedType::Unknown => {
|
||||||
|
warnings.push("Could not detect config type. Expected server, client, or combined (trx-rs.toml) layout.".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Format report
|
||||||
|
for w in &warnings {
|
||||||
|
writeln!(report, " warning: {}", w).unwrap();
|
||||||
|
}
|
||||||
|
for e in &errors {
|
||||||
|
writeln!(report, " error: {}", e).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() && warnings.is_empty() {
|
||||||
|
writeln!(report, " No issues found.").unwrap();
|
||||||
|
} else {
|
||||||
|
writeln!(
|
||||||
|
report,
|
||||||
|
" {} warning(s), {} error(s)",
|
||||||
|
warnings.len(),
|
||||||
|
errors.len()
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(report)
|
||||||
|
} else {
|
||||||
|
Err(report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_type(table: &toml_edit::Table) -> DetectedType {
|
||||||
|
if table.contains_key("trx-server") || table.contains_key("trx-client") {
|
||||||
|
return DetectedType::Combined;
|
||||||
|
}
|
||||||
|
let keys: Vec<&str> = table.iter().map(|(k, _)| k).collect();
|
||||||
|
|
||||||
|
let server_score = keys.iter().filter(|k| SERVER_KEYS.contains(k)).count();
|
||||||
|
let client_score = keys.iter().filter(|k| CLIENT_KEYS.contains(k)).count();
|
||||||
|
|
||||||
|
// Use distinguishing keys to break ties
|
||||||
|
if keys.contains(&"rig") || keys.contains(&"rigs") || keys.contains(&"listen") {
|
||||||
|
return DetectedType::Server;
|
||||||
|
}
|
||||||
|
if keys.contains(&"remote") || keys.contains(&"remotes") || keys.contains(&"frontends") {
|
||||||
|
return DetectedType::Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
if server_score > client_score {
|
||||||
|
DetectedType::Server
|
||||||
|
} else if client_score > server_score {
|
||||||
|
DetectedType::Client
|
||||||
|
} else if server_score > 0 {
|
||||||
|
DetectedType::Server
|
||||||
|
} else {
|
||||||
|
DetectedType::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_unknown_keys(
|
||||||
|
table: &toml_edit::Table,
|
||||||
|
known: &[&str],
|
||||||
|
prefix: &str,
|
||||||
|
warnings: &mut Vec<String>,
|
||||||
|
) {
|
||||||
|
for (key, _) in table.iter() {
|
||||||
|
if !known.contains(&key) {
|
||||||
|
warnings.push(format!("{}unknown key '{}'", prefix, key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_server_sections(
|
||||||
|
table: &toml_edit::Table,
|
||||||
|
prefix: &str,
|
||||||
|
warnings: &mut Vec<String>,
|
||||||
|
errors: &mut Vec<String>,
|
||||||
|
) {
|
||||||
|
if let Some(general) = table.get("general").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(general, SERVER_GENERAL_KEYS, &format!("{}[general].", prefix), warnings);
|
||||||
|
validate_log_level(general, &format!("{}[general]", prefix), errors);
|
||||||
|
validate_coordinates(general, &format!("{}[general]", prefix), errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rig) = table.get("rig").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(rig, RIG_KEYS, &format!("{}[rig].", prefix), warnings);
|
||||||
|
if let Some(access) = rig.get("access").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(access, ACCESS_KEYS, &format!("{}[rig.access].", prefix), warnings);
|
||||||
|
validate_access(access, &format!("{}[rig.access]", prefix), errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(listen) = table.get("listen").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(listen, LISTEN_KEYS, &format!("{}[listen].", prefix), warnings);
|
||||||
|
validate_port(listen, "port", &format!("{}[listen]", prefix), errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(audio) = table.get("audio").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(audio, AUDIO_KEYS, &format!("{}[audio].", prefix), warnings);
|
||||||
|
validate_port(audio, "port", &format!("{}[audio]", prefix), errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(behavior) = table.get("behavior").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(behavior, BEHAVIOR_KEYS, &format!("{}[behavior].", prefix), warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_client_sections(
|
||||||
|
table: &toml_edit::Table,
|
||||||
|
prefix: &str,
|
||||||
|
warnings: &mut Vec<String>,
|
||||||
|
errors: &mut Vec<String>,
|
||||||
|
) {
|
||||||
|
if let Some(general) = table.get("general").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(general, CLIENT_GENERAL_KEYS, &format!("{}[general].", prefix), warnings);
|
||||||
|
validate_log_level(general, &format!("{}[general]", prefix), errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(remote) = table.get("remote").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(remote, REMOTE_KEYS, &format!("{}[remote].", prefix), warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(frontends) = table.get("frontends").and_then(|v| v.as_table()) {
|
||||||
|
check_unknown_keys(frontends, FRONTENDS_KEYS, &format!("{}[frontends].", prefix), warnings);
|
||||||
|
if let Some(http) = frontends.get("http").and_then(|v| v.as_table()) {
|
||||||
|
validate_port(http, "port", &format!("{}[frontends.http]", prefix), errors);
|
||||||
|
}
|
||||||
|
if let Some(rigctl) = frontends.get("rigctl").and_then(|v| v.as_table()) {
|
||||||
|
validate_port(rigctl, "port", &format!("{}[frontends.rigctl]", prefix), errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Value validators ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn validate_log_level(table: &toml_edit::Table, context: &str, errors: &mut Vec<String>) {
|
||||||
|
if let Some(level) = table.get("log_level").and_then(|v| v.as_str()) {
|
||||||
|
if !["trace", "debug", "info", "warn", "error"].contains(&level) {
|
||||||
|
errors.push(format!(
|
||||||
|
"{}.log_level '{}' is invalid (expected: trace, debug, info, warn, error)",
|
||||||
|
context, level
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_coordinates(table: &toml_edit::Table, context: &str, errors: &mut Vec<String>) {
|
||||||
|
if let Some(lat) = table.get("latitude").and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64))) {
|
||||||
|
if !(-90.0..=90.0).contains(&lat) {
|
||||||
|
errors.push(format!("{}.latitude {} is out of range (-90..90)", context, lat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(lon) = table.get("longitude").and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64))) {
|
||||||
|
if !(-180.0..=180.0).contains(&lon) {
|
||||||
|
errors.push(format!(
|
||||||
|
"{}.longitude {} is out of range (-180..180)",
|
||||||
|
context, lon
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_lat = table.contains_key("latitude");
|
||||||
|
let has_lon = table.contains_key("longitude");
|
||||||
|
if has_lat != has_lon {
|
||||||
|
errors.push(format!(
|
||||||
|
"{}: latitude and longitude must be set together or both omitted",
|
||||||
|
context
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_port(
|
||||||
|
table: &toml_edit::Table,
|
||||||
|
key: &str,
|
||||||
|
context: &str,
|
||||||
|
errors: &mut Vec<String>,
|
||||||
|
) {
|
||||||
|
if let Some(port) = table.get(key).and_then(|v| v.as_integer()) {
|
||||||
|
if let Some(enabled) = table.get("enabled").and_then(|v| v.as_bool()) {
|
||||||
|
if enabled && port <= 0 {
|
||||||
|
errors.push(format!("{}.{} must be > 0 when enabled", context, key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !(0..=65535).contains(&port) {
|
||||||
|
errors.push(format!(
|
||||||
|
"{}.{} {} is out of range (0..65535)",
|
||||||
|
context, key, port
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_access(table: &toml_edit::Table, context: &str, errors: &mut Vec<String>) {
|
||||||
|
if let Some(access_type) = table.get("type").and_then(|v| v.as_str()) {
|
||||||
|
if !["serial", "tcp", "sdr"].contains(&access_type) {
|
||||||
|
errors.push(format!(
|
||||||
|
"{}.type '{}' is invalid (expected: serial, tcp, sdr)",
|
||||||
|
context, access_type
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
fn check_toml(content: &str) -> Result<String, String> {
|
||||||
|
let mut f = NamedTempFile::new().unwrap();
|
||||||
|
f.write_all(content.as_bytes()).unwrap();
|
||||||
|
check_file(f.path())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_server_config() {
|
||||||
|
let result = check_toml(
|
||||||
|
r#"
|
||||||
|
[general]
|
||||||
|
callsign = "W1AW"
|
||||||
|
log_level = "info"
|
||||||
|
|
||||||
|
[rig]
|
||||||
|
model = "ft817"
|
||||||
|
|
||||||
|
[rig.access]
|
||||||
|
type = "serial"
|
||||||
|
port = "/dev/ttyUSB0"
|
||||||
|
baud = 9600
|
||||||
|
|
||||||
|
[listen]
|
||||||
|
enabled = true
|
||||||
|
port = 4530
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let report = result.unwrap();
|
||||||
|
assert!(report.contains("Detected type: server"));
|
||||||
|
assert!(report.contains("No issues found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_client_config() {
|
||||||
|
let result = check_toml(
|
||||||
|
r#"
|
||||||
|
[general]
|
||||||
|
callsign = "W1AW"
|
||||||
|
|
||||||
|
[remote]
|
||||||
|
url = "localhost:4530"
|
||||||
|
|
||||||
|
[frontends.http]
|
||||||
|
enabled = true
|
||||||
|
port = 8080
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let report = result.unwrap();
|
||||||
|
assert!(report.contains("Detected type: client"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_combined_config() {
|
||||||
|
let result = check_toml(
|
||||||
|
r#"
|
||||||
|
[trx-server.general]
|
||||||
|
callsign = "W1AW"
|
||||||
|
|
||||||
|
[trx-client.general]
|
||||||
|
callsign = "W1AW"
|
||||||
|
|
||||||
|
[trx-client.remote]
|
||||||
|
url = "localhost:4530"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let report = result.unwrap();
|
||||||
|
assert!(report.contains("Detected type: combined"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_toml_syntax() {
|
||||||
|
let result = check_toml("this is not [valid toml");
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("TOML syntax error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unknown_key_warning() {
|
||||||
|
let result = check_toml(
|
||||||
|
r#"
|
||||||
|
[general]
|
||||||
|
callsign = "W1AW"
|
||||||
|
|
||||||
|
[rig]
|
||||||
|
model = "ft817"
|
||||||
|
|
||||||
|
[bogus_section]
|
||||||
|
foo = "bar"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let report = result.unwrap();
|
||||||
|
assert!(report.contains("unknown key 'bogus_section'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_log_level() {
|
||||||
|
let result = check_toml(
|
||||||
|
r#"
|
||||||
|
[general]
|
||||||
|
log_level = "verbose"
|
||||||
|
|
||||||
|
[rig]
|
||||||
|
model = "ft817"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let report = result.unwrap_err();
|
||||||
|
assert!(report.contains("log_level 'verbose' is invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_latitude_without_longitude() {
|
||||||
|
let result = check_toml(
|
||||||
|
r#"
|
||||||
|
[general]
|
||||||
|
latitude = 45.0
|
||||||
|
|
||||||
|
[rig]
|
||||||
|
model = "ft817"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let report = result.unwrap_err();
|
||||||
|
assert!(report.contains("latitude and longitude must be set together"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_latitude_out_of_range() {
|
||||||
|
let result = check_toml(
|
||||||
|
r#"
|
||||||
|
[general]
|
||||||
|
latitude = 95.0
|
||||||
|
longitude = 10.0
|
||||||
|
|
||||||
|
[rig]
|
||||||
|
model = "ft817"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let report = result.unwrap_err();
|
||||||
|
assert!(report.contains("latitude 95 is out of range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_access_type() {
|
||||||
|
let result = check_toml(
|
||||||
|
r#"
|
||||||
|
[rig]
|
||||||
|
model = "ft817"
|
||||||
|
|
||||||
|
[rig.access]
|
||||||
|
type = "usb"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let report = result.unwrap_err();
|
||||||
|
assert!(report.contains("type 'usb' is invalid"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: BSD-2-Clause
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
|
mod check;
|
||||||
mod detect;
|
mod detect;
|
||||||
mod prompts;
|
mod prompts;
|
||||||
mod writer;
|
mod writer;
|
||||||
@@ -24,6 +25,10 @@ struct Cli {
|
|||||||
/// Output file path (default: based on config type)
|
/// Output file path (default: based on config type)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Check an existing config file for syntax and structure errors
|
||||||
|
#[arg(long, value_name = "FILE")]
|
||||||
|
check: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -46,6 +51,19 @@ impl ConfigType {
|
|||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
if let Some(path) = &cli.check {
|
||||||
|
match check::check_file(path) {
|
||||||
|
Ok(report) => {
|
||||||
|
println!("{}", report);
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let config_type = if let Some(t) = &cli.r#type {
|
let config_type = if let Some(t) = &cli.r#type {
|
||||||
match t.as_str() {
|
match t.as_str() {
|
||||||
"server" => ConfigType::Server,
|
"server" => ConfigType::Server,
|
||||||
|
|||||||
Reference in New Issue
Block a user