From 7d7010ccca018c8c9cfe7beda75a809b2edb0fe4 Mon Sep 17 00:00:00 2001 From: Tim Bruijnzeels Date: Wed, 12 Feb 2020 17:31:14 +0100 Subject: [PATCH] Simplify ReportFormat in CLI (fixes: #132) --- src/cli/options.rs | 2 +- src/cli/report.rs | 601 ++++++++++++++++++--------------------------- src/daemon/test.rs | 4 +- 3 files changed, 243 insertions(+), 364 deletions(-) diff --git a/src/cli/options.rs b/src/cli/options.rs index 46e75cbb..87741cad 100644 --- a/src/cli/options.rs +++ b/src/cli/options.rs @@ -841,7 +841,7 @@ impl Options { .map(uri::Rsync::from_str) .unwrap()?; - if !rsync_base.to_string().ends_with("/") { + if !rsync_base.to_string().ends_with('/') { return Err(Error::general("URI for --rsync MUST end with a '/'")); } diff --git a/src/cli/report.rs b/src/cli/report.rs index 1c7be213..aba75d96 100644 --- a/src/cli/report.rs +++ b/src/cli/report.rs @@ -1,6 +1,7 @@ use std::str::{from_utf8_unchecked, FromStr}; use chrono::{DateTime, NaiveDateTime, Utc}; +use serde::Serialize; use rpki::x509::Time; @@ -56,13 +57,7 @@ impl ApiResponse { Ok(None) } else { match self { - ApiResponse::Health => { - if fmt == ReportFormat::Default { - Ok(None) - } else { - Err(ReportError::UnsupportedFormat) - } - } + ApiResponse::Health => Ok(None), ApiResponse::Info(info) => Ok(Some(info.report(fmt)?)), ApiResponse::CertAuths(list) => Ok(Some(list.report(fmt)?)), ApiResponse::CertAuthInfo(info) => Ok(Some(info.report(fmt)?)), @@ -93,11 +88,9 @@ impl ApiResponse { /// This type defines the format to use when representing the api response #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ReportFormat { - Default, // the normal format for this data type, usually json None, Json, Text, - Xml, } impl FromStr for ReportFormat { @@ -108,7 +101,6 @@ impl FromStr for ReportFormat { "none" => Ok(ReportFormat::None), "json" => Ok(ReportFormat::Json), "text" => Ok(ReportFormat::Text), - "xml" => Ok(ReportFormat::Xml), _ => Err(ReportError::UnrecognisedFormat(s.to_string())), } } @@ -130,457 +122,344 @@ pub enum ReportError { /// This trait should be implemented by all api responses, so that the /// response can be formatted for users. -trait Report { - fn report(&self, format: ReportFormat) -> Result; +trait Report: Serialize { + fn text(&self) -> Result; + + fn json(&self) -> String { + serde_json::to_string_pretty(self).unwrap() + } + + fn report(&self, format: ReportFormat) -> Result { + match format { + ReportFormat::None => Ok("".to_string()), + ReportFormat::Json => Ok(self.json()), + ReportFormat::Text => self.text(), + } + } } impl Report for CertAuthList { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Default | ReportFormat::Json => { - Ok(serde_json::to_string_pretty(self).unwrap()) - } - ReportFormat::Text => { - let mut res = String::new(); - for ca in self.cas() { - res.push_str(&format!("{}\n", ca.handle())); - } - - Ok(res) - } - _ => Err(ReportError::UnsupportedFormat), + fn text(&self) -> Result { + let mut res = String::new(); + for ca in self.cas() { + res.push_str(&format!("{}\n", ca.handle())); } + + Ok(res) } } impl Report for CertAuthInfo { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Default | ReportFormat::Json => { - Ok(serde_json::to_string_pretty(self).unwrap()) - } - ReportFormat::Text => { - let mut res = String::new(); + fn text(&self) -> Result { + let mut res = String::new(); - res.push_str(&format!("Name: {}\n", self.handle())); - res.push_str("\n"); + res.push_str(&format!("Name: {}\n", self.handle())); + res.push_str("\n"); - if let Some(repo_info) = self.repo_info() { - let base_uri = repo_info.base_uri(); - let rrdp_uri = repo_info.rpki_notify(); - res.push_str(&format!("Base uri: {}\n", base_uri)); - res.push_str(&format!("RRDP uri: {}\n", rrdp_uri)); - } else { - res.push_str("No repository configured.") - } - res.push_str("\n"); - - res.push_str(&format!("ID cert PEM:\n{}\n", self.id_cert().pem())); - res.push_str(&format!("Hash: {}\n", self.id_cert().hash())); - res.push_str("\n"); - - let resources = self.resources(); - if resources.is_empty() { - res.push_str("Total resources: \n"); - } else { - res.push_str("Total resources:\n"); - res.push_str(&format!(" ASNs: {}\n", resources.asn())); - res.push_str(&format!(" IPv4: {}\n", resources.v4())); - res.push_str(&format!(" IPv6: {}\n", resources.v6())); - } - res.push_str("\n"); - - fn print_objects(res: &mut String, objects: &CurrentObjects) { - for object in objects.names() { - res.push_str(&format!(" {}\n", object)); - } - } - - res.push_str("Parents:\n"); - if !self.parents().is_empty() { - for parent in self.parents().iter() { - res.push_str(&format!("{}\n", parent)); - } - res.push_str("\n"); - } else { - res.push_str("\n") - } - - for (name, rc) in self.resource_classes() { - res.push_str(&format!("Resource Class: {}\n", name,)); - res.push_str(&format!("Parent: {}\n", rc.parent_handle())); - res.push_str(&format!("{}", rc.keys())); - - res.push_str("Current objects:\n"); - print_objects(&mut res, rc.current_objects()); - res.push_str("\n"); - } - - res.push_str("Children:\n"); - if !self.children().is_empty() { - for child_handle in self.children() { - res.push_str(&format!("{}\n", child_handle)); - } - } else { - res.push_str("\n"); - } - - Ok(res) - } - _ => Err(ReportError::UnsupportedFormat), + if let Some(repo_info) = self.repo_info() { + let base_uri = repo_info.base_uri(); + let rrdp_uri = repo_info.rpki_notify(); + res.push_str(&format!("Base uri: {}\n", base_uri)); + res.push_str(&format!("RRDP uri: {}\n", rrdp_uri)); + } else { + res.push_str("No repository configured.") } + res.push_str("\n"); + + res.push_str(&format!("ID cert PEM:\n{}\n", self.id_cert().pem())); + res.push_str(&format!("Hash: {}\n", self.id_cert().hash())); + res.push_str("\n"); + + let resources = self.resources(); + if resources.is_empty() { + res.push_str("Total resources: \n"); + } else { + res.push_str("Total resources:\n"); + res.push_str(&format!(" ASNs: {}\n", resources.asn())); + res.push_str(&format!(" IPv4: {}\n", resources.v4())); + res.push_str(&format!(" IPv6: {}\n", resources.v6())); + } + res.push_str("\n"); + + fn print_objects(res: &mut String, objects: &CurrentObjects) { + for object in objects.names() { + res.push_str(&format!(" {}\n", object)); + } + } + + res.push_str("Parents:\n"); + if !self.parents().is_empty() { + for parent in self.parents().iter() { + res.push_str(&format!("{}\n", parent)); + } + res.push_str("\n"); + } else { + res.push_str("\n") + } + + for (name, rc) in self.resource_classes() { + res.push_str(&format!("Resource Class: {}\n", name,)); + res.push_str(&format!("Parent: {}\n", rc.parent_handle())); + res.push_str(&format!("{}", rc.keys())); + + res.push_str("Current objects:\n"); + print_objects(&mut res, rc.current_objects()); + res.push_str("\n"); + } + + res.push_str("Children:\n"); + if !self.children().is_empty() { + for child_handle in self.children() { + res.push_str(&format!("{}\n", child_handle)); + } + } else { + res.push_str("\n"); + } + + Ok(res) } } impl Report for CertAuthHistory { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - ReportFormat::Default | ReportFormat::Text => Ok(format!("{}", self)), - _ => Err(ReportError::UnsupportedFormat), - } + fn text(&self) -> Result { + Ok(format!("{}", self)) } } impl Report for ParentCaContact { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - ReportFormat::Default | ReportFormat::Text | ReportFormat::Xml => { - let mut res = String::new(); - match self { - ParentCaContact::Ta(details) => { - res.push_str(&format!("{}", details.tal())); - } - ParentCaContact::Embedded => { - res.push_str("Embedded parent"); - } - ParentCaContact::Rfc6492(response) => { - let bytes = response.encode_vec(); - let xml = unsafe { from_utf8_unchecked(&bytes) }; - res.push_str(xml); - } - } - Ok(res) + fn text(&self) -> Result { + let mut res = String::new(); + match self { + ParentCaContact::Ta(details) => { + res.push_str(&format!("{}", details.tal())); + } + ParentCaContact::Embedded => { + res.push_str("Embedded parent"); + } + ParentCaContact::Rfc6492(response) => { + let bytes = response.encode_vec(); + let xml = unsafe { from_utf8_unchecked(&bytes) }; + res.push_str(xml); } - _ => Err(ReportError::UnsupportedFormat), } + Ok(res) } } impl Report for ChildCaInfo { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Default | ReportFormat::Json => { - Ok(serde_json::to_string_pretty(self).unwrap()) - } - ReportFormat::Text => Ok(self.to_string()), - _ => Err(ReportError::UnsupportedFormat), - } + fn text(&self) -> Result { + Ok(self.to_string()) } } impl Report for PublisherList { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Default | ReportFormat::Json => { - Ok(serde_json::to_string_pretty(self).unwrap()) - } - ReportFormat::Text => { - let mut res = String::new(); + fn text(&self) -> Result { + let mut res = String::new(); - res.push_str("Publishers: "); - let mut first = true; - for p in self.publishers() { - if !first { - res.push_str(", "); - } else { - first = false; - } - res.push_str(p.handle().as_str()); - } - Ok(res) + res.push_str("Publishers: "); + let mut first = true; + for p in self.publishers() { + if !first { + res.push_str(", "); + } else { + first = false; } - _ => Err(ReportError::UnsupportedFormat), + res.push_str(p.handle().as_str()); } + Ok(res) } } impl Report for RepoStats { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - ReportFormat::Default | ReportFormat::Text => { - let mut res = String::new(); + fn text(&self) -> Result { + let mut res = String::new(); - if let Some(update) = self.last_update() { - res.push_str(&format!("RRDP updated: {}\n", update.to_rfc3339())); - } - res.push_str(&format!("RRDP session: {}\n", self.session())); - res.push_str(&format!("RRDP serial: {}\n", self.serial())); - res.push_str("\n"); - res.push_str("Publisher, Objects, Size, Last Updated\n"); - for (publisher, stats) in self.get_publishers() { - res.push_str(&format!( - "{}, {}, {}, ", - publisher, - stats.objects(), - stats.size() - )); - match stats.last_update() { - None => res.push_str("never\n"), - Some(update) => res.push_str(&format!("{}\n", update.to_rfc3339())), - } - } - - Ok(res) - } - _ => Err(ReportError::UnsupportedFormat), + if let Some(update) = self.last_update() { + res.push_str(&format!("RRDP updated: {}\n", update.to_rfc3339())); } + res.push_str(&format!("RRDP session: {}\n", self.session())); + res.push_str(&format!("RRDP serial: {}\n", self.serial())); + res.push_str("\n"); + res.push_str("Publisher, Objects, Size, Last Updated\n"); + for (publisher, stats) in self.get_publishers() { + res.push_str(&format!( + "{}, {}, {}, ", + publisher, + stats.objects(), + stats.size() + )); + match stats.last_update() { + None => res.push_str("never\n"), + Some(update) => res.push_str(&format!("{}\n", update.to_rfc3339())), + } + } + + Ok(res) } } impl Report for PublisherDetails { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Default | ReportFormat::Json => { - Ok(serde_json::to_string_pretty(self).unwrap()) - } - ReportFormat::Text => { - let mut res = String::new(); + fn text(&self) -> Result { + let mut res = String::new(); - res.push_str(&format!("handle: {}\n", self.handle())); - res.push_str(&format!("id: {}", self.id_cert().ski_hex())); - res.push_str(&format!("base uri: {}\n", self.base_uri().to_string())); + res.push_str(&format!("handle: {}\n", self.handle())); + res.push_str(&format!("id: {}", self.id_cert().ski_hex())); + res.push_str(&format!("base uri: {}\n", self.base_uri().to_string())); - Ok(res) - } - _ => Err(ReportError::UnsupportedFormat), - } + Ok(res) } } impl Report for Vec { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Default | ReportFormat::Json => { - Ok(serde_json::to_string_pretty(self).unwrap()) - } - ReportFormat::Text => { - let mut res = String::new(); + fn text(&self) -> Result { + let mut res = String::new(); - res.push_str("Clients: "); - for client in self.iter() { - let handle = client.handle(); - let auth = client.auth(); - let ski = auth.cert().ski_hex(); + res.push_str("Clients: "); + for client in self.iter() { + let handle = client.handle(); + let auth = client.auth(); + let ski = auth.cert().ski_hex(); - res.push_str(&format!(" Handle: {}, Cert (ski): {}\n", handle, ski)); - } - Ok(res) - } - _ => Err(ReportError::UnsupportedFormat), + res.push_str(&format!(" Handle: {}, Cert (ski): {}\n", handle, ski)); } + Ok(res) } } impl Report for rfc8183::RepositoryResponse { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Text | ReportFormat::Xml | ReportFormat::Default => { - let bytes = self.encode_vec(); - let xml = unsafe { from_utf8_unchecked(&bytes) }; - - Ok(xml.to_string()) - } - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - _ => Err(ReportError::UnsupportedFormat), - } + fn text(&self) -> Result { + let bytes = self.encode_vec(); + let xml = unsafe { from_utf8_unchecked(&bytes) }; + Ok(xml.to_string()) } } impl Report for rfc8183::ChildRequest { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Text | ReportFormat::Xml | ReportFormat::Default => { - let bytes = self.encode_vec(); - let xml = unsafe { from_utf8_unchecked(&bytes) }; - - Ok(xml.to_string()) - } - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - _ => Err(ReportError::UnsupportedFormat), - } + fn text(&self) -> Result { + let bytes = self.encode_vec(); + let xml = unsafe { from_utf8_unchecked(&bytes) }; + Ok(xml.to_string()) } } impl Report for rfc8183::PublisherRequest { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Text | ReportFormat::Xml | ReportFormat::Default => { - let bytes = self.encode_vec(); - let xml = unsafe { from_utf8_unchecked(&bytes) }; - - Ok(xml.to_string()) - } - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - _ => Err(ReportError::UnsupportedFormat), - } + fn text(&self) -> Result { + let bytes = self.encode_vec(); + let xml = unsafe { from_utf8_unchecked(&bytes) }; + Ok(xml.to_string()) } } impl Report for Vec { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Default | ReportFormat::Json => { - Ok(serde_json::to_string_pretty(self).unwrap()) - } - ReportFormat::Text => { - let mut res = String::new(); - for a in self.iter() { - res.push_str(&format!("{}\n", a)); - } - Ok(res) - } - _ => Err(ReportError::UnsupportedFormat), + fn text(&self) -> Result { + let mut res = String::new(); + for a in self.iter() { + res.push_str(&format!("{}\n", a)); } + Ok(res) } } impl Report for CaRepoDetails { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - ReportFormat::Default | ReportFormat::Text => { - let mut res = String::new(); + fn text(&self) -> Result { + let mut res = String::new(); - res.push_str("Repository Details:\n"); - match self.contact() { - RepositoryContact::Embedded(repo_info) => { - res.push_str(" type: embedded\n"); - res.push_str(&format!(" base_uri: {}\n", repo_info.base_uri())); - res.push_str(&format!(" rpki_notify: {}\n", repo_info.rpki_notify())); - } - RepositoryContact::Rfc8181(response) => { - res.push_str(" type: remote\n"); - res.push_str(&format!(" service uri: {}\n", response.service_uri())); - let repo_info = response.repo_info(); - res.push_str(&format!(" base_uri: {}\n", repo_info.base_uri())); - res.push_str(&format!(" rpki_notify: {}\n", repo_info.rpki_notify())); - } - } - - res.push_str("\n"); - - Ok(res) + res.push_str("Repository Details:\n"); + match self.contact() { + RepositoryContact::Embedded(repo_info) => { + res.push_str(" type: embedded\n"); + res.push_str(&format!(" base_uri: {}\n", repo_info.base_uri())); + res.push_str(&format!(" rpki_notify: {}\n", repo_info.rpki_notify())); + } + RepositoryContact::Rfc8181(response) => { + res.push_str(" type: remote\n"); + res.push_str(&format!(" service uri: {}\n", response.service_uri())); + let repo_info = response.repo_info(); + res.push_str(&format!(" base_uri: {}\n", repo_info.base_uri())); + res.push_str(&format!(" rpki_notify: {}\n", repo_info.rpki_notify())); } - _ => Err(ReportError::UnsupportedFormat), } + + res.push_str("\n"); + + Ok(res) } } impl Report for CurrentRepoState { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - ReportFormat::Default | ReportFormat::Text => match &self { - CurrentRepoState::Error(e) => Ok(format!("Error contacting repo! => {}\n", e)), - CurrentRepoState::List(list) => { - let mut res = String::new(); - res.push_str("Available and publishing objects:\n"); - let elements = list.elements(); - if elements.is_empty() { - res.push_str(" \n"); - } else { - for el in elements.iter() { - res.push_str(&format!(" {} {}\n", el.hash(), el.uri())); - } + fn text(&self) -> Result { + match &self { + CurrentRepoState::Error(e) => Ok(format!("Error contacting repo! => {}\n", e)), + CurrentRepoState::List(list) => { + let mut res = String::new(); + res.push_str("Available and publishing objects:\n"); + let elements = list.elements(); + if elements.is_empty() { + res.push_str(" \n"); + } else { + for el in elements.iter() { + res.push_str(&format!(" {} {}\n", el.hash(), el.uri())); } - Ok(res) } - }, - _ => Err(ReportError::UnsupportedFormat), + Ok(res) + } } } } impl Report for CertAuthIssues { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Default | ReportFormat::Text => { - let mut res = String::new(); - if self.is_empty() { - res.push_str("no issues found\n") - } else { - if let Some(repo_issue) = self.repo_issue() { - res.push_str(&format!("Repository Issue: {}\n", repo_issue)); - } - let parent_issues = self.parent_issues(); - if !parent_issues.is_empty() { - for (parent, issue) in parent_issues.iter() { - res.push_str(&format!("Parent '{}' has issue: {}\n", parent, issue)); - } - } - } - Ok(res) + fn text(&self) -> Result { + let mut res = String::new(); + if self.is_empty() { + res.push_str("no issues found\n") + } else { + if let Some(repo_issue) = self.repo_issue() { + res.push_str(&format!("Repository Issue: {}\n", repo_issue)); + } + let parent_issues = self.parent_issues(); + if !parent_issues.is_empty() { + for (parent, issue) in parent_issues.iter() { + res.push_str(&format!("Parent '{}' has issue: {}\n", parent, issue)); + } } - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - _ => Err(ReportError::UnsupportedFormat), } + Ok(res) } } impl Report for AllCertAuthIssues { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Default | ReportFormat::Text => { - let cas = self.cas(); - let mut res = String::new(); - if cas.is_empty() { - res.push_str("no issues found\n"); - } else { - for (ca, issues) in cas.iter() { - res.push_str(&format!("Found issue for CA '{}':\n", ca)); + fn text(&self) -> Result { + let cas = self.cas(); + let mut res = String::new(); + if cas.is_empty() { + res.push_str("no issues found\n"); + } else { + for (ca, issues) in cas.iter() { + res.push_str(&format!("Found issue for CA '{}':\n", ca)); - if let Some(repo_issue) = issues.repo_issue() { - res.push_str(&format!(" Repository Issue: {}\n", repo_issue)); - } - let parent_issues = issues.parent_issues(); - if !parent_issues.is_empty() { - for (parent, issue) in parent_issues.iter() { - res.push_str(&format!( - " Parent '{}' has issue: {}\n", - parent, issue - )); - } - } + if let Some(repo_issue) = issues.repo_issue() { + res.push_str(&format!(" Repository Issue: {}\n", repo_issue)); + } + let parent_issues = issues.parent_issues(); + if !parent_issues.is_empty() { + for (parent, issue) in parent_issues.iter() { + res.push_str(&format!(" Parent '{}' has issue: {}\n", parent, issue)); } } - Ok(res) } - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - _ => Err(ReportError::UnsupportedFormat), } + Ok(res) } } impl Report for ServerInfo { - fn report(&self, format: ReportFormat) -> Result { - match format { - ReportFormat::Default | ReportFormat::Text => { - let dt = DateTime::::from_utc( - NaiveDateTime::from_timestamp(self.started(), 0), - Utc, - ); - let started = Time::new(dt); - Ok(format!( - "Version: {}\nStarted: {}", - self.version(), - started.to_rfc3339() - )) - } - ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), - _ => Err(ReportError::UnsupportedFormat), - } + fn text(&self) -> Result { + let dt = DateTime::::from_utc(NaiveDateTime::from_timestamp(self.started(), 0), Utc); + let started = Time::new(dt); + Ok(format!( + "Version: {}\nStarted: {}", + self.version(), + started.to_rfc3339() + )) } } diff --git a/src/daemon/test.rs b/src/daemon/test.rs index 62107fba..b4ae6f5f 100644 --- a/src/daemon/test.rs +++ b/src/daemon/test.rs @@ -89,7 +89,7 @@ fn health_check() -> Result { let krillc_opts = Options::new( test::https("https://localhost:3000/"), "secret", - ReportFormat::Default, + ReportFormat::Text, Command::Health, ); @@ -113,7 +113,7 @@ fn krill_pubd_health() -> Result { let krillc_opts = Options::new( test::https("https://localhost:3001/"), "secret", - ReportFormat::Default, + ReportFormat::Text, Command::Health, );