Simplify ReportFormat in CLI (fixes: #132)

This commit is contained in:
Tim Bruijnzeels
2020-02-12 17:31:14 +01:00
parent c6ff655517
commit 7d7010ccca
3 changed files with 243 additions and 364 deletions
+1 -1
View File
@@ -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 '/'"));
}
+240 -361
View File
@@ -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<String, ReportError>;
trait Report: Serialize {
fn text(&self) -> Result<String, ReportError>;
fn json(&self) -> String {
serde_json::to_string_pretty(self).unwrap()
}
fn report(&self, format: ReportFormat) -> Result<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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: <none>\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("<none>\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("<none>\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: <none>\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("<none>\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("<none>\n");
}
Ok(res)
}
}
impl Report for CertAuthHistory {
fn report(&self, format: ReportFormat) -> Result<String, ReportError> {
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<String, ReportError> {
Ok(format!("{}", self))
}
}
impl Report for ParentCaContact {
fn report(&self, format: ReportFormat) -> Result<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
Ok(self.to_string())
}
}
impl Report for PublisherList {
fn report(&self, format: ReportFormat) -> Result<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<ClientInfo> {
fn report(&self, format: ReportFormat) -> Result<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
let bytes = self.encode_vec();
let xml = unsafe { from_utf8_unchecked(&bytes) };
Ok(xml.to_string())
}
}
impl Report for Vec<RoaDefinition> {
fn report(&self, format: ReportFormat) -> Result<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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(" <nothing>\n");
} else {
for el in elements.iter() {
res.push_str(&format!(" {} {}\n", el.hash(), el.uri()));
}
fn text(&self) -> Result<String, ReportError> {
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(" <nothing>\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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
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<String, ReportError> {
match format {
ReportFormat::Default | ReportFormat::Text => {
let dt = DateTime::<Utc>::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<String, ReportError> {
let dt = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.started(), 0), Utc);
let started = Time::new(dt);
Ok(format!(
"Version: {}\nStarted: {}",
self.version(),
started.to_rfc3339()
))
}
}
+2 -2
View File
@@ -89,7 +89,7 @@ fn health_check() -> Result<ApiResponse, Error> {
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<ApiResponse, Error> {
let krillc_opts = Options::new(
test::https("https://localhost:3001/"),
"secret",
ReportFormat::Default,
ReportFormat::Text,
Command::Health,
);