From 6fbd736c5f8b2002e07beb84a768e12a8e2c5149 Mon Sep 17 00:00:00 2001 From: Avery Harnish <avery@apollographql.com> Date: Thu, 21 Jan 2021 15:43:44 -0600 Subject: [PATCH] feat(rover): scaffolds app-level error codes and suggestions --- Cargo.lock | 1 + Cargo.toml | 3 +- src/bin/rover.rs | 19 +++++++--- src/cli.rs | 2 +- src/client.rs | 3 +- src/command/config/auth.rs | 6 ++-- src/command/config/clear.rs | 2 +- src/command/config/delete.rs | 2 +- src/command/config/list.rs | 2 +- src/command/config/mod.rs | 2 +- src/command/config/show.rs | 12 ++----- src/command/graph/check.rs | 6 ++-- src/command/graph/fetch.rs | 5 ++- src/command/graph/mod.rs | 2 +- src/command/graph/push.rs | 2 +- src/command/install/mod.rs | 4 +-- src/command/subgraph/check.rs | 18 +++++----- src/command/subgraph/delete.rs | 4 ++- src/command/subgraph/fetch.rs | 2 +- src/command/subgraph/mod.rs | 2 +- src/command/subgraph/push.rs | 6 ++-- src/error/metadata/code.rs | 10 ++++++ src/error/metadata/mod.rs | 43 +++++++++++++++++++++++ src/error/metadata/suggestion.rs | 26 ++++++++++++++ src/error/mod.rs | 60 ++++++++++++++++++++++++++++++++ src/lib.rs | 3 ++ src/utils/loaders.rs | 8 ++--- src/utils/parsers.rs | 10 +++--- 28 files changed, 204 insertions(+), 61 deletions(-) create mode 100644 src/error/metadata/code.rs create mode 100644 src/error/metadata/mod.rs create mode 100644 src/error/metadata/suggestion.rs create mode 100644 src/error/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 0d4f5938a..c2e865f88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,6 +1453,7 @@ dependencies = [ name = "rover" version = "0.0.1-rc.4" dependencies = [ + "ansi_term 0.12.1", "anyhow", "assert_cmd", "assert_fs", diff --git a/Cargo.toml b/Cargo.toml index 0636fe4d9..1bb7edf61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,9 @@ robot-panic = { path = "./crates/robot-panic" } binstall = { path = "./installers/binstall" } # crates.io deps -anyhow = "1.0.36" +anyhow = "1.0.38" atty = "0.2.14" +ansi_term = "0.12.1" console = "0.14.0" heck = "0.3.2" prettytable-rs = "0.8.0" diff --git a/src/bin/rover.rs b/src/bin/rover.rs index 8bb89cb74..7d4f1021d 100644 --- a/src/bin/rover.rs +++ b/src/bin/rover.rs @@ -1,20 +1,29 @@ -use anyhow::Result; +use command::RoverStdout; use robot_panic::setup_panic; use rover::*; use sputnik::Session; use structopt::StructOpt; -use std::thread; +use std::{process, thread}; -fn main() -> Result<()> { +fn main() { setup_panic!(); + if let Err(error) = run() { + tracing::debug!(?error); + eprintln!("{}", error); + process::exit(1) + } else { + process::exit(0) + } +} +fn run() -> Result<()> { let app = cli::Rover::from_args(); timber::init(app.log_level); tracing::trace!(command_structure = ?app); // attempt to create a new `Session` to capture anonymous usage data - let result = match Session::new(&app) { + let output: RoverStdout = match Session::new(&app) { // if successful, report the usage data in the background Ok(session) => { // kicks off the reporting on a background thread @@ -45,6 +54,6 @@ fn main() -> Result<()> { Err(_) => app.run(), }?; - result.print(); + output.print(); Ok(()) } diff --git a/src/cli.rs b/src/cli.rs index 52ed8c97b..2f90c475a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,9 +1,9 @@ -use anyhow::Result; use serde::Serialize; use structopt::StructOpt; use crate::env::{RoverEnv, RoverEnvKey}; use crate::stringify::from_display; +use crate::Result; use crate::{ client::StudioClientConfig, command::{self, RoverStdout}, diff --git a/src/client.rs b/src/client.rs index 26bf5e653..c706fe8fb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,5 @@ -use anyhow::Result; +use crate::Result; + use houston as config; use rover_client::blocking::StudioClient; diff --git a/src/command/config/auth.rs b/src/command/config/auth.rs index c72ee5cdf..7bf864b0a 100644 --- a/src/command/config/auth.rs +++ b/src/command/config/auth.rs @@ -1,4 +1,3 @@ -use anyhow::{Context, Error, Result}; use console::{self, style}; use serde::Serialize; use structopt::StructOpt; @@ -7,6 +6,7 @@ use config::Profile; use houston as config; use crate::command::RoverStdout; +use crate::{anyhow, Context, Result}; #[derive(Debug, Serialize, StructOpt)] /// Authenticate a configuration profile with an API key @@ -27,7 +27,7 @@ pub struct Auth { impl Auth { pub fn run(&self, config: config::Config) -> Result<RoverStdout> { - let api_key = api_key_prompt().context("Failed to read API key from terminal")?; + let api_key = api_key_prompt()?; Profile::set_api_key(&self.profile_name, &config, &api_key) .context("Failed while saving API key")?; Profile::get_api_key(&self.profile_name, &config) @@ -50,7 +50,7 @@ fn api_key_prompt() -> Result<String> { if is_valid(&api_key) { Ok(api_key) } else { - Err(Error::msg("Received an empty API Key. Please try again.")) + Err(anyhow!("Received an empty API Key. Please try again.").into()) } } diff --git a/src/command/config/clear.rs b/src/command/config/clear.rs index e6d6449fc..9a6c5a6db 100644 --- a/src/command/config/clear.rs +++ b/src/command/config/clear.rs @@ -1,8 +1,8 @@ -use anyhow::{Context, Result}; use serde::Serialize; use structopt::StructOpt; use crate::command::RoverStdout; +use crate::{Context, Result}; use houston as config; diff --git a/src/command/config/delete.rs b/src/command/config/delete.rs index f117156b2..305171941 100644 --- a/src/command/config/delete.rs +++ b/src/command/config/delete.rs @@ -1,10 +1,10 @@ -use anyhow::{Context, Result}; use serde::Serialize; use structopt::StructOpt; use houston as config; use crate::command::RoverStdout; +use crate::{Context, Result}; #[derive(Debug, Serialize, StructOpt)] /// Delete a configuration profile diff --git a/src/command/config/list.rs b/src/command/config/list.rs index 20a9d1fdf..f78d4ea32 100644 --- a/src/command/config/list.rs +++ b/src/command/config/list.rs @@ -1,7 +1,7 @@ -use anyhow::{Context, Result}; use serde::Serialize; use structopt::StructOpt; +use crate::{Context, Result}; use houston as config; use crate::command::RoverStdout; diff --git a/src/command/config/mod.rs b/src/command/config/mod.rs index 1ca4b20c5..c0fa5694c 100644 --- a/src/command/config/mod.rs +++ b/src/command/config/mod.rs @@ -4,13 +4,13 @@ mod delete; mod list; mod show; -use anyhow::Result; use serde::Serialize; use structopt::StructOpt; use houston as config; use crate::command::RoverStdout; +use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Config { diff --git a/src/command/config/show.rs b/src/command/config/show.rs index e54f8ee98..b935fdcc4 100644 --- a/src/command/config/show.rs +++ b/src/command/config/show.rs @@ -1,10 +1,10 @@ -use anyhow::Result; use serde::Serialize; use structopt::StructOpt; use houston as config; use crate::command::RoverStdout; +use crate::Result; #[derive(Debug, Serialize, StructOpt)] /// View a configuration profile's details /// @@ -24,15 +24,7 @@ impl Show { sensitive: self.sensitive, }; - let profile = config::Profile::load(&self.name, &config, opts).map_err(|e| { - let context = match e { - config::HoustonProblem::NoNonSensitiveConfigFound(_) => { - "Could not show any profile information. Try re-running with the `--sensitive` flag" - } - _ => "Could not load profile", - }; - anyhow::anyhow!(e).context(context) - })?; + let profile = config::Profile::load(&self.name, &config, opts)?; tracing::info!("{}: {}", &self.name, profile); Ok(RoverStdout::None) diff --git a/src/command/graph/check.rs b/src/command/graph/check.rs index d46ee06e2..a4feaee0a 100644 --- a/src/command/graph/check.rs +++ b/src/command/graph/check.rs @@ -1,4 +1,3 @@ -use anyhow::{Context, Result}; use prettytable::{cell, row, Table}; use serde::Serialize; use structopt::StructOpt; @@ -9,6 +8,7 @@ use crate::client::StudioClientConfig; use crate::command::RoverStdout; use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{parse_graph_ref, parse_schema_source, GraphRef, SchemaSource}; +use crate::{Context, Result}; #[derive(Debug, Serialize, StructOpt)] pub struct Check { @@ -68,8 +68,8 @@ impl Check { match num_failures { 0 => Ok(RoverStdout::None), - 1 => Err(anyhow::anyhow!("Encountered 1 failure.")), - _ => Err(anyhow::anyhow!("Encountered {} failures.", num_failures)), + 1 => Err(anyhow::anyhow!("Encountered 1 failure.").into()), + _ => Err(anyhow::anyhow!("Encountered {} failures.", num_failures).into()), } } } diff --git a/src/command/graph/fetch.rs b/src/command/graph/fetch.rs index 559fb3a7b..c1114d868 100644 --- a/src/command/graph/fetch.rs +++ b/src/command/graph/fetch.rs @@ -1,4 +1,3 @@ -use anyhow::{Context, Result}; use serde::Serialize; use structopt::StructOpt; @@ -7,6 +6,7 @@ use rover_client::query::graph::fetch; use crate::client::StudioClientConfig; use crate::command::RoverStdout; use crate::utils::parsers::{parse_graph_ref, GraphRef}; +use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Fetch { @@ -39,8 +39,7 @@ impl Fetch { variant: Some(self.graph.variant.clone()), }, &client, - ) - .context("Failed while fetching from Apollo Studio")?; + )?; Ok(RoverStdout::SDL(sdl)) } diff --git a/src/command/graph/mod.rs b/src/command/graph/mod.rs index 204821813..ec4c7061f 100644 --- a/src/command/graph/mod.rs +++ b/src/command/graph/mod.rs @@ -2,12 +2,12 @@ mod check; mod fetch; mod push; -use anyhow::Result; use serde::Serialize; use structopt::StructOpt; use crate::client::StudioClientConfig; use crate::command::RoverStdout; +use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Graph { diff --git a/src/command/graph/push.rs b/src/command/graph/push.rs index 2eb33ec82..3deb816d7 100644 --- a/src/command/graph/push.rs +++ b/src/command/graph/push.rs @@ -1,4 +1,3 @@ -use anyhow::{Context, Result}; use serde::Serialize; use structopt::StructOpt; @@ -8,6 +7,7 @@ use crate::client::StudioClientConfig; use crate::command::RoverStdout; use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{parse_graph_ref, parse_schema_source, GraphRef, SchemaSource}; +use crate::{Context, Result}; #[derive(Debug, Serialize, StructOpt)] pub struct Push { diff --git a/src/command/install/mod.rs b/src/command/install/mod.rs index 8dcabb4a4..eada2bafd 100644 --- a/src/command/install/mod.rs +++ b/src/command/install/mod.rs @@ -1,10 +1,10 @@ -use anyhow::{anyhow, Context, Result}; use serde::Serialize; use structopt::StructOpt; use binstall::Installer; use crate::command::RoverStdout; +use crate::{anyhow, Context, Result}; use std::env; use std::path::PathBuf; @@ -36,7 +36,7 @@ impl Install { } Ok(RoverStdout::None) } else { - Err(anyhow!("Failed to get the current executable's path.")) + Err(anyhow!("Failed to get the current executable's path.").into()) } } } diff --git a/src/command/subgraph/check.rs b/src/command/subgraph/check.rs index 20c8aeb85..5b00f2d79 100644 --- a/src/command/subgraph/check.rs +++ b/src/command/subgraph/check.rs @@ -1,8 +1,8 @@ -use anyhow::{Context, Result}; use prettytable::{cell, row, Table}; use serde::Serialize; use structopt::StructOpt; +use crate::{Context, Result}; use rover_client::query::subgraph::check; use crate::client::StudioClientConfig; @@ -113,13 +113,12 @@ fn handle_checks(check_result: check::CheckResult) -> Result<RoverStdout> { match num_failures { 0 => Ok(RoverStdout::None), - 1 => Err(anyhow::anyhow!( - "Encountered 1 failure while checking your subgraph." - )), + 1 => Err(anyhow::anyhow!("Encountered 1 failure while checking your subgraph.").into()), _ => Err(anyhow::anyhow!( "Encountered {} failures while checking your subgraph.", num_failures - )), + ) + .into()), } } @@ -133,12 +132,13 @@ fn handle_composition_errors( } match num_failures { 0 => Ok(RoverStdout::None), - 1 => Err(anyhow::anyhow!( - "Encountered 1 composition error while composing the subgraph." - )), + 1 => Err( + anyhow::anyhow!("Encountered 1 composition error while composing the subgraph.").into(), + ), _ => Err(anyhow::anyhow!( "Encountered {} composition errors while composing the subgraph.", num_failures - )), + ) + .into()), } } diff --git a/src/command/subgraph/delete.rs b/src/command/subgraph/delete.rs index 14c8c492e..634c86917 100644 --- a/src/command/subgraph/delete.rs +++ b/src/command/subgraph/delete.rs @@ -1,8 +1,10 @@ use crate::client::StudioClientConfig; use crate::command::RoverStdout; use crate::utils::parsers::{parse_graph_ref, GraphRef}; -use anyhow::Result; +use crate::Result; + use rover_client::query::subgraph::delete::{self, DeleteServiceResponse}; + use serde::Serialize; use structopt::StructOpt; diff --git a/src/command/subgraph/fetch.rs b/src/command/subgraph/fetch.rs index 7704eb04b..f7438feaf 100644 --- a/src/command/subgraph/fetch.rs +++ b/src/command/subgraph/fetch.rs @@ -1,4 +1,3 @@ -use anyhow::{Context, Result}; use serde::Serialize; use structopt::StructOpt; @@ -7,6 +6,7 @@ use rover_client::query::subgraph::fetch; use crate::client::StudioClientConfig; use crate::command::RoverStdout; use crate::utils::parsers::{parse_graph_ref, GraphRef}; +use crate::{Context, Result}; #[derive(Debug, Serialize, StructOpt)] pub struct Fetch { diff --git a/src/command/subgraph/mod.rs b/src/command/subgraph/mod.rs index 5d89bcdc7..a9d79b8bf 100644 --- a/src/command/subgraph/mod.rs +++ b/src/command/subgraph/mod.rs @@ -3,12 +3,12 @@ mod delete; mod fetch; mod push; -use anyhow::Result; use serde::Serialize; use structopt::StructOpt; use crate::client::StudioClientConfig; use crate::command::RoverStdout; +use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Subgraph { diff --git a/src/command/subgraph/push.rs b/src/command/subgraph/push.rs index 58c49d4fc..acd734774 100644 --- a/src/command/subgraph/push.rs +++ b/src/command/subgraph/push.rs @@ -1,13 +1,13 @@ -use anyhow::{Context, Result}; -use rover_client::query::subgraph::push::{self, PushPartialSchemaResponse}; use serde::Serialize; use structopt::StructOpt; use crate::client::StudioClientConfig; use crate::command::RoverStdout; - use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{parse_graph_ref, parse_schema_source, GraphRef, SchemaSource}; +use crate::{Context, Result}; + +use rover_client::query::subgraph::push::{self, PushPartialSchemaResponse}; #[derive(Debug, Serialize, StructOpt)] pub struct Push { diff --git a/src/error/metadata/code.rs b/src/error/metadata/code.rs new file mode 100644 index 000000000..e94266c47 --- /dev/null +++ b/src/error/metadata/code.rs @@ -0,0 +1,10 @@ +use std::fmt::{self, Display}; + +#[derive(Debug)] +pub enum Code {} + +impl Display for Code { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{:?}", &self) + } +} diff --git a/src/error/metadata/mod.rs b/src/error/metadata/mod.rs new file mode 100644 index 000000000..9685e4c37 --- /dev/null +++ b/src/error/metadata/mod.rs @@ -0,0 +1,43 @@ +mod code; +mod suggestion; + +use code::Code; +use suggestion::Suggestion; + +use houston::HoustonProblem; +use rover_client::RoverClientError; + +#[derive(Default, Debug)] +pub struct Metadata { + pub suggestion: Option<Suggestion>, + pub code: Option<Code>, +} + +impl From<&mut anyhow::Error> for Metadata { + fn from(error: &mut anyhow::Error) -> Self { + if let Some(rover_client_error) = error.downcast_ref::<RoverClientError>() { + let (suggestion, code) = match rover_client_error { + RoverClientError::InvalidJSON(_) + | RoverClientError::InvalidHeaderName(_) + | RoverClientError::InvalidHeaderValue(_) + | RoverClientError::SendRequest(_) + | RoverClientError::NoCheckData + | RoverClientError::InvalidSeverity => (Some(Suggestion::SubmitIssue), None), + _ => (None, None), + }; + return Metadata { suggestion, code }; + } + + if let Some(houston_problem) = error.downcast_ref::<HoustonProblem>() { + let (suggestion, code) = match houston_problem { + HoustonProblem::NoNonSensitiveConfigFound(_) => { + (Some(Suggestion::RerunWithSensitive), None) + } + _ => (None, None), + }; + return Metadata { suggestion, code }; + } + + Metadata::default() + } +} diff --git a/src/error/metadata/suggestion.rs b/src/error/metadata/suggestion.rs new file mode 100644 index 000000000..a5dd9d6f7 --- /dev/null +++ b/src/error/metadata/suggestion.rs @@ -0,0 +1,26 @@ +use std::fmt::{self, Display}; + +use ansi_term::Colour::{Cyan, Yellow}; + +#[derive(Debug)] +pub enum Suggestion { + SubmitIssue, + RerunWithSensitive, +} + +impl Display for Suggestion { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let suggestion = match self { + Suggestion::SubmitIssue => { + format!("This error was unexpected! Please submit an issue with any relevant details about what you were trying to do: {}", Cyan.normal().paint("https://github.com/apollographql/rover/issues/new")) + } + Suggestion::RerunWithSensitive => { + format!( + "Try re-running this command with the {} flag", + Yellow.normal().paint("'--sensitive'") + ) + } + }; + write!(formatter, "{}", &suggestion) + } +} diff --git a/src/error/mod.rs b/src/error/mod.rs new file mode 100644 index 000000000..77a7111e3 --- /dev/null +++ b/src/error/mod.rs @@ -0,0 +1,60 @@ +mod metadata; + +pub use anyhow::{anyhow, Context}; +pub(crate) use metadata::Metadata; + +pub type Result<T> = std::result::Result<T, RoverError>; + +use ansi_term::Colour::{Cyan, Red}; + +use std::borrow::BorrowMut; +use std::fmt::{self, Debug, Display}; + +/// A specialized `Error` type for Rover that wraps `anyhow` +/// and provides some extra `Metadata` for end users depending +/// on the speicif error they encountered. +#[derive(Debug)] +pub struct RoverError { + error: anyhow::Error, + metadata: Metadata, +} + +impl RoverError { + pub fn new<E>(error: E) -> Self + where + E: Into<anyhow::Error>, + { + let mut error = error.into(); + let metadata = Metadata::from(error.borrow_mut()); + + Self { error, metadata } + } +} + +impl Display for RoverError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let error_descriptor_message = if let Some(code) = &self.metadata.code { + format!("error[{}]:", code) + } else { + "error:".to_string() + }; + let error_descriptor = Red.bold().paint(&error_descriptor_message); + writeln!(formatter, "{} {}", error_descriptor, &self.error)?; + + if let Some(suggestion) = &self.metadata.suggestion { + let mut suggestion_descriptor_message = "".to_string(); + for _ in 0..error_descriptor_message.len() + 1 { + suggestion_descriptor_message.push(' '); + } + let suggestion_descriptor = Cyan.bold().paint(&suggestion_descriptor_message); + write!(formatter, "{} {}", suggestion_descriptor, suggestion)?; + } + Ok(()) + } +} + +impl<E: Into<anyhow::Error>> From<E> for RoverError { + fn from(error: E) -> Self { + Self::new(error) + } +} diff --git a/src/lib.rs b/src/lib.rs index 10a817c44..63c359ab7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,9 @@ pub mod cli; mod client; pub mod command; pub mod env; +mod error; mod stringify; mod telemetry; mod utils; + +pub use error::{anyhow, Context, Result}; diff --git a/src/utils/loaders.rs b/src/utils/loaders.rs index 47897e9cf..89f1029e7 100644 --- a/src/utils/loaders.rs +++ b/src/utils/loaders.rs @@ -1,5 +1,6 @@ use crate::utils::parsers::SchemaSource; -use anyhow::{Context, Result}; +use crate::{anyhow, Context, Result}; + use std::io::Read; use std::path::Path; @@ -20,10 +21,7 @@ pub fn load_schema_from_flag(loc: &SchemaSource, mut stdin: impl Read) -> Result let contents = std::fs::read_to_string(path)?; Ok(contents) } else { - Err(anyhow::anyhow!( - "Invalid path. No file found at {}", - path.display() - )) + Err(anyhow!("Invalid path. No file found at {}", path.display()).into()) } } } diff --git a/src/utils/parsers.rs b/src/utils/parsers.rs index d18967d21..eafca7241 100644 --- a/src/utils/parsers.rs +++ b/src/utils/parsers.rs @@ -1,8 +1,8 @@ -use anyhow::Result; -use anyhow::*; use regex::Regex; use std::path::PathBuf; +use crate::{anyhow, Result}; + #[derive(Debug, PartialEq)] pub enum SchemaSource { Stdin, @@ -13,9 +13,7 @@ pub fn parse_schema_source(loc: &str) -> Result<SchemaSource> { if loc == "-" { Ok(SchemaSource::Stdin) } else if loc.is_empty() { - Err(anyhow::anyhow!( - "The path provided to find a schema is empty" - )) + Err(anyhow!("The path provided to find a schema is empty").into()) } else { let path = PathBuf::from(loc); Ok(SchemaSource::File(path)) @@ -52,7 +50,7 @@ pub fn parse_graph_ref(graph_id: &str) -> Result<GraphRef> { variant: variant.to_string(), }) } else { - Err(anyhow!("Graph IDs must be in the format <NAME> or <NAME>@<VARIANT>, where <NAME> can only contain letters, numbers, or the characters `-` or `_`, and must be 64 characters or less. <VARIANT> must be 64 characters or less.")) + Err(anyhow!("Graph IDs must be in the format <NAME> or <NAME>@<VARIANT>, where <NAME> can only contain letters, numbers, or the characters `-` or `_`, and must be 64 characters or less. <VARIANT> must be 64 characters or less.").into()) } }