#[macro_use] extern crate serde_derive; #[macro_use] extern crate derive_error_chain; extern crate serde_yaml; use std::fs::File; use std::path::Path; fn main() { //read the files //let paths = [Path::new("test.yml")]; let filenames: Vec<_> = std::env::args_os().skip(1).collect(); let hosts: Vec = filenames.iter() .map(Path::new) .map(|path| Host::from_file(&path)) .map(|result| match result { Ok(val) => val, Err(err) => panic!("{:#}", err), }) .collect(); //build dot-graph let nodes = hosts.iter().map(Host::to_dot_node); let edges = hosts.iter().flat_map(Host::to_dot_edge); //render it println!("graph antennen {{"); for node in nodes { println!("{}", node); } for edge in edges { println!("{}", edge); } println!("}}"); } #[derive(Debug, ErrorChain)] pub enum ParseError { #[error_chain(foreign)] IO(std::io::Error), #[error_chain(custom)] #[error_chain(description = r#"|_| "File invalid/incomplete yaml""#)] #[error_chain(display = r#"|&(ref path, ref err)| write!(f, "failed at file {:?} with {}", path, err)"#)] Yaml((std::ffi::OsString, serde_yaml::Error)), #[error_chain(custom)] #[error_chain(description = r#"|_| "Host file structure broken""#)] #[error_chain(display = r#"|&(ref name, ref err)| write!(f, "the antenna config of {} is wrong: {}", name, err)"#)] Structure((String, String)), } #[derive(Debug)] struct Host { name: String, ip: String, mac: Option, kind: HostKind, } impl Host { fn from_file(path: &Path) -> Result { RawHost::from_file(path)?.parse() } fn to_dot_node(&self) -> String { let mac = self.mac.clone().unwrap_or("no mac".into()); let mut attributes = vec! [ "shape=record".to_string() , format!("label=\"{{\'{name}\'|{typ}|{{{ip}|{mac}}}|{{{ipv6}}}}}\"" , name = self.name, typ=self.kind.name(), ip=self.ip , mac=mac, ipv6="no ipv6") , "style=filled".into() ]; // type-specific handling match self.kind { HostKind::Client {coordinates, ref subnet, .. } => { attributes.push("fillcolor=lightgray".into()) ; if let Some(coordinates) = coordinates { attributes.push(format!("pos=\"{breitengrad},{längengrad}\"" , längengrad=coordinates[0], breitengrad=coordinates[1])) } }, _ => (), }; let attributes = attributes.join(", "); format!("\"{name}\" [{attributes}]", name=self.name, attributes=attributes) } fn to_dot_edge(&self) -> Option { match self.kind { HostKind::Client { ref parent, .. } => format!("\"{name}\" -- \"{parent}\"", name=self.name, parent=parent).into(), _ => None, } } } #[derive(Debug)] enum HostKind { Client { subnet: String, coordinates: Option<[f64;2]>, parent: String }, AccessPoint, Other, } impl HostKind { fn name(&self) -> &'static str { use HostKind::*; match *self { Client { .. } => "Client", AccessPoint => "AccessPoint", Other => "Other", } } } #[derive(Deserialize,Debug)] struct RawHost { #[serde(skip)] name: Option, ansible_host: String, #[serde(rename = "type")] kind: String, mac: Option, client_of: Option, coordinate: Option<[f64;2]>, subnet: Option, } impl RawHost { fn parse(self) -> Result { let name : String = self.name .ok_or(ParseError::Structure(("???".into(),"got no file name? weird.".into())))? .into_string().unwrap() .split('.') .next().unwrap().into(); Ok(Host { name: name.clone() , ip: self.ansible_host , mac: self.mac , kind: match self.kind.as_ref() { "wlan-ap" => HostKind::AccessPoint , "client" => HostKind::Client { subnet: self.subnet.ok_or(ParseError::Structure((name.clone(), "is client, has no subnet".into())))? //, coordinates: self.coordinate.ok_or(ParseError::Structure("is client, has no coordinates".into()))? , coordinates: self.coordinate , parent: self.client_of.ok_or(ParseError::Structure((name.clone(), "is client, has no client_of".into())))? } , _ => HostKind::Other } }) } fn from_file(path: &Path) -> Result { let fd = File::open(path)?; let mut antenna : Self = match serde_yaml::from_reader(&fd) { Ok(val) => val, Err(err) => return Err(ParseError::Yaml((path.as_os_str().to_owned(), err)).into()), }; antenna.name = path.file_name().map(|a|a.to_owned()); Ok(antenna) } }