use crate::{AppError, AppResult};
use anyhow::anyhow;
use camino::Utf8PathBuf;
use clap::Parser;
use std::collections::HashMap;
use std::str::FromStr;
#[derive(Parser)]
#[clap(author, version, about, after_help = CLAP_AFTER_HELP)]
pub(crate) struct Args {
#[clap(long = "virtual", id = "VIRTUAL", verbatim_doc_comment)]
pub(crate) virtuals: Vec<Virtual>,
#[clap(long = "physical", id = "PHYSICAL", verbatim_doc_comment)]
pub(crate) physicals: Vec<Physical>,
#[clap(long = "route", id = "ROUTE", verbatim_doc_comment)]
pub(crate) routes: Vec<Route>,
}
const CLAP_AFTER_HELP: &str = "
EXAMPLES:
Share a physical serial port with two virtual serial ports.
Data sent from virtual serial port 0 is sent to the physical serial port but not to virtual
serial port 1. Similarly, data sent from virtual serial port 1 is sent to the physical serial
port but not to virtual serial port 0. Data received fromt the physical serial port is sent to
both virtual serial ports.
vsp-router \\
--virtual 0 \\
--virtual 1 \\
--physical 2:/dev/ttyUSB0,115200 \\
--route 0:2 \\
--route 1:2 \\
--route 2:0 \\
--route 2:1
";
#[derive(Clone, Debug)]
pub(crate) struct Virtual {
pub(crate) id: String,
pub(crate) path: Utf8PathBuf,
}
#[derive(Clone, Debug)]
pub(crate) struct Physical {
pub(crate) id: String,
pub(crate) path: Utf8PathBuf,
pub(crate) baud_rate: u32,
}
#[derive(Clone, Debug)]
pub(crate) struct Route {
pub(crate) src: String,
pub(crate) dst: String,
}
impl Args {
pub(crate) fn validate(&self) -> AppResult<()> {
self.check_duplicate_ids()?;
self.check_route_ids()
}
fn check_route_ids(&self) -> AppResult<()> {
let ids = self
.virtuals
.iter()
.map(|virtual_| virtual_.id.as_str())
.chain(self.physicals.iter().map(|physical| physical.id.as_str()))
.collect::<Vec<&str>>();
for route in &self.routes {
if !ids.contains(&route.src.as_str()) {
Err(anyhow!(
"the source ID '{}' in route '{}:{}' was not found",
route.src,
route.src,
route.dst
))?;
}
if !ids.contains(&route.dst.as_str()) {
Err(anyhow!(
"the destination ID '{}' in route '{}:{}' was not found",
route.dst,
route.src,
route.dst
))?;
}
}
Ok(())
}
fn check_duplicate_ids(&self) -> AppResult<()> {
let duplicate_ids = self
.virtuals
.iter()
.map(|virtual_| &virtual_.id)
.chain(self.physicals.iter().map(|physical| &physical.id))
.fold(HashMap::new(), |mut map, id| {
*map.entry(id).or_insert(0) += 1;
map
})
.iter()
.filter_map(
|(id, &count)| {
if count > 1 {
Some(id.as_str())
} else {
None
}
},
)
.collect::<Vec<&str>>();
if !duplicate_ids.is_empty() {
Err(anyhow!(
"the following IDs were used more than once: {}",
duplicate_ids.join(", ")
))
} else {
Ok(())
}
}
}
impl FromStr for Virtual {
type Err = AppError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once(':') {
None => {
let path = Utf8PathBuf::from(s);
let id = path
.file_name()
.ok_or_else(|| anyhow!("invalid path '{s}'"))?
.to_owned();
Ok(Self { id, path })
}
Some((id, path)) => {
let id = id.to_owned();
let path = Utf8PathBuf::from(path);
Ok(Self { id, path })
}
}
}
}
impl FromStr for Physical {
type Err = AppError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (remainder, baud_rate) = match s.split_once(',') {
None => (s, 9600),
Some((remainder, baud_rate)) => {
let baud_rate = baud_rate.parse()?;
(remainder, baud_rate)
}
};
let id_path = Virtual::from_str(remainder)?;
Ok(Physical {
id: id_path.id,
path: id_path.path,
baud_rate,
})
}
}
impl FromStr for Route {
type Err = AppError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (src, dst) = s
.split_once(':')
.ok_or_else(|| anyhow!("invalid route '{s}'"))?;
Ok(Self {
src: src.to_string(),
dst: dst.to_string(),
})
}
}