diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 599f97fa4..1b117879a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -116,8 +116,8 @@ A minimal command in Rover would be laid out exactly like this: pub struct MyNewCommand { } impl MyNewCommand { - pub fn run(&self) -> Result { - Ok(RoverStdout::None) + pub fn run(&self) -> Result { + Ok(RoverOutput::None) } } ``` @@ -128,16 +128,16 @@ For our `graph hello` command, we'll add a new `hello.rs` file under `src/comman use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Hello { } impl Hello { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { eprintln!("Hello, world!"); - Ok(RoverStdout::None) + Ok(RoverOutput::None) } } ``` @@ -348,7 +348,7 @@ Before we go any further, lets make sure everything is set up properly. We're go It should look something like this (you should make sure you are following the style of other commands when creating new ones): ```rust -pub fn run(&self, client_config: StudioClientConfig) -> Result { +pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -362,7 +362,10 @@ pub fn run(&self, client_config: StudioClientConfig) -> Result { }, &client, )?; - Ok(RoverStdout::PlainText(deleted_at)) + println!("{:?}", deleted_at); + + // TODO: Add a new output type! + Ok(RoverOutput::None) } ``` @@ -399,17 +402,32 @@ Unfortunately this is not the cleanest API and doesn't match the pattern set by You'll want to define all of the types scoped to this command in `types.rs`, and re-export them from the top level `hello` module, and nothing else. -##### `RoverStdout` +##### `RoverOutput` -Now that you can actually execute the `hello::run` query and return its result, you should create a new variant of `RoverStdout` in `src/command/output.rs` that is not `PlainText`. Your new variant should print the descriptor using the `print_descriptor` function, and print the raw content using `print_content`. +Now that you can actually execute the `hello::run` query and return its result, you should create a new variant of `RoverOutput` in `src/command/output.rs` that is not `None`. Your new variant should print the descriptor using the `print_descriptor` function, and print the raw content using `print_content`. -To do so, change the line `Ok(RoverStdout::PlainText(deleted_at))` to `Ok(RoverStdout::DeletedAt(deleted_at))`, add a new `DeletedAt(String)` variant to `RoverStdout`, and then match on it in `pub fn print(&self)`: +To do so, change the line `Ok(RoverOutput::None)` to `Ok(RoverOutput::DeletedAt(deleted_at))`, add a new `DeletedAt(String)` variant to `RoverOutput`, and then match on it in `pub fn print(&self)` and `pub fn get_json(&self)`: ```rust -... -RoverStdout::DeletedAt(timestamp) => { - print_descriptor("Deleted At"); - print_content(×tamp); +pub fn print(&self) { + match self { + ... + RoverOutput::DeletedAt(timestamp) => { + print_descriptor("Deleted At"); + print_content(×tamp); + } + ... + } +} + +pub fn get_json(&self) -> Value { + match self { + ... + RoverOutput::DeletedAt(timestamp) => { + json!({ "deleted_at": timestamp.to_string() }) + } + ... + } } ``` diff --git a/Cargo.lock b/Cargo.lock index 8360b5340..8d5d179ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,6 +83,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +[[package]] +name = "assert-json-diff" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "assert_cmd" version = "1.0.7" @@ -321,6 +331,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi", ] @@ -1915,6 +1926,7 @@ version = "0.1.8" dependencies = [ "ansi_term 0.12.1", "anyhow", + "assert-json-diff", "assert_cmd", "assert_fs", "atty", @@ -1970,6 +1982,7 @@ dependencies = [ "indoc", "online", "pretty_assertions", + "prettytable-rs", "regex", "reqwest", "sdl-encoder", diff --git a/Cargo.toml b/Cargo.toml index 01dcb44c3..3bdb7ad04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ url = { version = "2.2.2", features = ["serde"] } [dev-dependencies] assert_cmd = "1.0.7" assert_fs = "1.0.3" +assert-json-diff = "2.0.1" predicates = "2.0.0" reqwest = { version = "0.11.4", default-features = false, features = ["blocking", "native-tls-vendored"] } serial_test = "0.5.0" diff --git a/crates/rover-client/Cargo.toml b/crates/rover-client/Cargo.toml index 99d56f702..e1c5315de 100644 --- a/crates/rover-client/Cargo.toml +++ b/crates/rover-client/Cargo.toml @@ -12,12 +12,13 @@ houston = {path = "../houston"} # crates.io deps camino = "1" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } git-url-parse = "0.3.1" git2 = { version = "0.13.20", default-features = false, features = ["vendored-openssl"] } graphql_client = "0.9" http = "0.2" humantime = "2.1.0" +prettytable-rs = "0.8.0" reqwest = { version = "0.11", default-features = false, features = ["blocking", "brotli", "gzip", "json", "native-tls-vendored"] } regex = "1" sdl-encoder = {path = "../sdl-encoder"} diff --git a/crates/rover-client/src/error.rs b/crates/rover-client/src/error.rs index 7e9a1fb49..47f2568c8 100644 --- a/crates/rover-client/src/error.rs +++ b/crates/rover-client/src/error.rs @@ -1,7 +1,7 @@ use reqwest::Url; use thiserror::Error; -use crate::shared::{CheckResponse, CompositionError, GraphRef}; +use crate::shared::{BuildErrors, CheckResponse, GraphRef}; /// RoverClientError represents all possible failures that can occur during a client request. #[derive(Error, Debug)] @@ -96,19 +96,21 @@ pub enum RoverClientError { GraphNotFound { graph_ref: GraphRef }, /// if someone attempts to get a core schema from a supergraph that has - /// no composition results we return this error. - #[error( - "No supergraph SDL exists for \"{graph_ref}\" because its subgraphs failed to compose." - )] - NoCompositionPublishes { + /// no successful build in the API, we return this error. + #[error("No supergraph SDL exists for \"{graph_ref}\" because its subgraphs failed to build.")] + NoSupergraphBuilds { graph_ref: GraphRef, - composition_errors: Vec, + source: BuildErrors, }, - #[error("{}", subgraph_composition_error_msg(.composition_errors))] - SubgraphCompositionErrors { + #[error("Encountered {} while trying to build a supergraph.", .source.length_string())] + BuildErrors { source: BuildErrors }, + + #[error("Encountered {} while trying to build subgraph \"{subgraph}\" into supergraph \"{graph_ref}\".", .source.length_string())] + SubgraphBuildErrors { + subgraph: String, graph_ref: GraphRef, - composition_errors: Vec, + source: BuildErrors, }, /// This error occurs when the Studio API returns no implementing services for a graph @@ -142,7 +144,7 @@ pub enum RoverClientError { /// While checking the proposed schema, we encountered changes that would break existing operations // we nest the CheckResponse here because we want to print the entire response even // if there were failures - #[error("{}", check_response_error_msg(.check_response))] + #[error("{}", operation_check_error_msg(.check_response))] OperationCheckFailure { graph_ref: GraphRef, check_response: CheckResponse, @@ -174,29 +176,14 @@ pub enum RoverClientError { SubgraphIntrospectionNotAvailable, } -fn subgraph_composition_error_msg(composition_errors: &[CompositionError]) -> String { - let num_failures = composition_errors.len(); - if num_failures == 0 { - unreachable!("No composition errors were encountered while composing the supergraph."); - } - let mut msg = String::new(); - msg.push_str(&match num_failures { - 1 => "Encountered 1 composition error while composing the supergraph.".to_string(), - _ => format!( - "Encountered {} composition errors while composing the supergraph.", - num_failures - ), - }); - msg -} - -fn check_response_error_msg(check_response: &CheckResponse) -> String { - let plural = match check_response.num_failures { +fn operation_check_error_msg(check_response: &CheckResponse) -> String { + let failure_count = check_response.get_failure_count(); + let plural = match failure_count { 1 => "", _ => "s", }; format!( - "This operation has encountered {} change{} that would break existing clients.", - check_response.num_failures, plural + "This operation check has encountered {} schema change{} that would break operations from existing client traffic.", + failure_count, plural ) } diff --git a/crates/rover-client/src/operations/graph/check/runner.rs b/crates/rover-client/src/operations/graph/check/runner.rs index 83e0873a0..152ab808e 100644 --- a/crates/rover-client/src/operations/graph/check/runner.rs +++ b/crates/rover-client/src/operations/graph/check/runner.rs @@ -1,7 +1,5 @@ use crate::blocking::StudioClient; -use crate::operations::graph::check::types::{ - GraphCheckInput, MutationChangeSeverity, MutationResponseData, -}; +use crate::operations::graph::check::types::{GraphCheckInput, MutationResponseData}; use crate::shared::{CheckResponse, GraphRef}; use crate::RoverClientError; @@ -45,25 +43,19 @@ fn get_check_response_from_data( let diff_to_previous = service.check_schema.diff_to_previous; - let number_of_checked_operations = diff_to_previous.number_of_checked_operations.unwrap_or(0); + let operation_check_count = diff_to_previous.number_of_checked_operations.unwrap_or(0) as u64; - let change_severity = diff_to_previous.severity.into(); + let result = diff_to_previous.severity.into(); let mut changes = Vec::with_capacity(diff_to_previous.changes.len()); - let mut num_failures = 0; for change in diff_to_previous.changes { - if let MutationChangeSeverity::FAILURE = change.severity { - num_failures += 1; - } changes.push(change.into()); } - let check_response = CheckResponse { + CheckResponse::try_new( target_url, - number_of_checked_operations, + operation_check_count, changes, - change_severity, - num_failures, - }; - - check_response.check_for_failures(graph_ref) + result, + graph_ref, + ) } diff --git a/crates/rover-client/src/operations/graph/publish/mod.rs b/crates/rover-client/src/operations/graph/publish/mod.rs index 4d4a2c184..e4fe3abb2 100644 --- a/crates/rover-client/src/operations/graph/publish/mod.rs +++ b/crates/rover-client/src/operations/graph/publish/mod.rs @@ -2,4 +2,6 @@ mod runner; mod types; pub use runner::run; -pub use types::{GraphPublishInput, GraphPublishResponse}; +pub use types::{ + ChangeSummary, FieldChanges, GraphPublishInput, GraphPublishResponse, TypeChanges, +}; diff --git a/crates/rover-client/src/operations/graph/publish/runner.rs b/crates/rover-client/src/operations/graph/publish/runner.rs index c46032d20..4ff0fbaec 100644 --- a/crates/rover-client/src/operations/graph/publish/runner.rs +++ b/crates/rover-client/src/operations/graph/publish/runner.rs @@ -1,4 +1,5 @@ use crate::blocking::StudioClient; +use crate::operations::graph::publish::types::{ChangeSummary, FieldChanges, TypeChanges}; use crate::operations::graph::publish::{GraphPublishInput, GraphPublishResponse}; use crate::shared::GraphRef; use crate::RoverClientError; @@ -75,39 +76,66 @@ fn build_response( // which very well may have changes. For this, we'll just look at the code // first and handle the response as if there was `None` for the diff let change_summary = if publish_response.code == "NO_CHANGES" { - build_change_summary(None) + ChangeSummary::none() } else { - build_change_summary(publish_response.tag.unwrap().diff_to_previous) + let diff = publish_response + .tag + .ok_or_else(|| RoverClientError::MalformedResponse { + null_field: "service.upload_schema.tag".to_string(), + })? + .diff_to_previous; + + if let Some(diff) = diff { + diff.into() + } else { + ChangeSummary::none() + } }; Ok(GraphPublishResponse { - schema_hash: hash, + api_schema_hash: hash, change_summary, }) } -type ChangeDiff = graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagDiffToPrevious; +type QueryChangeDiff = + graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagDiffToPrevious; -/// builds a string-representation of the diff between two schemas -/// e.g. ` [Fields: +2 -1 △0, Types: +4 -0 △7]` or `[No Changes]` -fn build_change_summary(diff: Option) -> String { - match diff { - None => "[No Changes]".to_string(), - Some(diff) => { - let changes = diff.change_summary; - let fields = format!( - "Fields: +{} -{} △ {}", - changes.field.additions, changes.field.removals, changes.field.edits - ); - let types = format!( - "Types: +{} -{} △ {}", - changes.type_.additions, changes.type_.removals, changes.type_.edits - ); - format!("[{}, {}]", fields, types) +impl From for ChangeSummary { + fn from(input: QueryChangeDiff) -> Self { + Self { + field_changes: input.change_summary.field.into(), + type_changes: input.change_summary.type_.into(), } } } +type QueryFieldChanges = + graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagDiffToPreviousChangeSummaryField; + +impl From for FieldChanges { + fn from(input: QueryFieldChanges) -> Self { + Self::with_diff( + input.additions as u64, + input.removals as u64, + input.edits as u64, + ) + } +} + +type QueryTypeChanges = + graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagDiffToPreviousChangeSummaryType; + +impl From for TypeChanges { + fn from(input: QueryTypeChanges) -> Self { + Self::with_diff( + input.additions as u64, + input.removals as u64, + input.edits as u64, + ) + } +} + #[cfg(test)] mod tests { use super::*; @@ -199,8 +227,8 @@ mod tests { assert_eq!( output.unwrap(), GraphPublishResponse { - schema_hash: "123456".to_string(), - change_summary: "[No Changes]".to_string(), + api_schema_hash: "123456".to_string(), + change_summary: ChangeSummary::none(), } ); } @@ -251,14 +279,20 @@ mod tests { } } }); - let diff_to_previous: ChangeDiff = serde_json::from_value(json_diff).unwrap(); - let output = build_change_summary(Some(diff_to_previous)); - assert_eq!(output, "[Fields: +3 -1 △ 0, Types: +4 -0 △ 2]".to_string()) + let diff_to_previous: QueryChangeDiff = serde_json::from_value(json_diff).unwrap(); + let output: ChangeSummary = diff_to_previous.into(); + assert_eq!( + output.to_string(), + "[Fields: +3 -1 △ 0, Types: +4 -0 △ 2]".to_string() + ) } #[test] fn build_change_summary_works_with_no_changes() { - assert_eq!(build_change_summary(None), "[No Changes]".to_string()) + assert_eq!( + ChangeSummary::none().to_string(), + "[No Changes]".to_string() + ) } fn mock_graph_ref() -> GraphRef { diff --git a/crates/rover-client/src/operations/graph/publish/types.rs b/crates/rover-client/src/operations/graph/publish/types.rs index 8fee1a4cd..b51b0808b 100644 --- a/crates/rover-client/src/operations/graph/publish/types.rs +++ b/crates/rover-client/src/operations/graph/publish/types.rs @@ -1,6 +1,10 @@ use crate::operations::graph::publish::runner::graph_publish_mutation; use crate::shared::{GitContext, GraphRef}; +use serde::Serialize; + +use std::fmt; + #[derive(Clone, Debug, PartialEq)] pub struct GraphPublishInput { pub graph_ref: GraphRef, @@ -33,8 +37,116 @@ impl From for GraphPublishContextInput { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Serialize, Debug, PartialEq)] pub struct GraphPublishResponse { - pub schema_hash: String, - pub change_summary: String, + pub api_schema_hash: String, + #[serde(flatten)] + pub change_summary: ChangeSummary, +} + +#[derive(Clone, Serialize, Debug, PartialEq)] +pub struct ChangeSummary { + pub field_changes: FieldChanges, + pub type_changes: TypeChanges, +} + +impl ChangeSummary { + pub(crate) fn none() -> ChangeSummary { + ChangeSummary { + field_changes: FieldChanges::none(), + type_changes: TypeChanges::none(), + } + } + + pub(crate) fn is_none(&self) -> bool { + self.field_changes.is_none() && self.type_changes.is_none() + } +} + +impl fmt::Display for ChangeSummary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.is_none() { + write!(f, "[No Changes]") + } else { + write!(f, "[{}, {}]", &self.field_changes, &self.type_changes) + } + } +} + +#[derive(Clone, Serialize, Debug, PartialEq)] +pub struct FieldChanges { + pub additions: u64, + pub removals: u64, + pub edits: u64, +} + +impl FieldChanges { + pub(crate) fn none() -> FieldChanges { + FieldChanges { + additions: 0, + removals: 0, + edits: 0, + } + } + + pub(crate) fn with_diff(additions: u64, removals: u64, edits: u64) -> FieldChanges { + FieldChanges { + additions, + removals, + edits, + } + } + + pub(crate) fn is_none(&self) -> bool { + self.additions == 0 && self.removals == 0 && self.edits == 0 + } +} + +impl fmt::Display for FieldChanges { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Fields: +{} -{} △ {}", + &self.additions, &self.removals, &self.edits + ) + } +} + +#[derive(Clone, Serialize, Debug, PartialEq)] +pub struct TypeChanges { + pub additions: u64, + pub removals: u64, + pub edits: u64, +} + +impl TypeChanges { + pub(crate) fn none() -> TypeChanges { + TypeChanges { + additions: 0, + removals: 0, + edits: 0, + } + } + + pub(crate) fn with_diff(additions: u64, removals: u64, edits: u64) -> TypeChanges { + TypeChanges { + additions, + removals, + edits, + } + } + + pub(crate) fn is_none(&self) -> bool { + self.additions == 0 && self.removals == 0 && self.edits == 0 + } +} + +impl fmt::Display for TypeChanges { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Types: +{} -{} △ {}", + &self.additions, &self.removals, &self.edits + ) + } } diff --git a/crates/rover-client/src/operations/subgraph/check/runner.rs b/crates/rover-client/src/operations/subgraph/check/runner.rs index 0df7f1fa7..4b34dbba2 100644 --- a/crates/rover-client/src/operations/subgraph/check/runner.rs +++ b/crates/rover-client/src/operations/subgraph/check/runner.rs @@ -4,7 +4,7 @@ use crate::operations::{ config::is_federated::{self, IsFederatedInput}, subgraph::check::types::MutationResponseData, }; -use crate::shared::{CheckResponse, CompositionError, GraphRef, SchemaChange}; +use crate::shared::{BuildError, CheckResponse, GraphRef, SchemaChange}; use crate::RoverClientError; use graphql_client::*; @@ -32,6 +32,7 @@ pub fn run( client: &StudioClient, ) -> Result { let graph_ref = input.graph_ref.clone(); + let subgraph = input.subgraph.clone(); // This response is used to check whether or not the current graph is federated. let is_federated = is_federated::run( IsFederatedInput { @@ -47,12 +48,13 @@ pub fn run( } let variables = input.into(); let data = client.post::(variables)?; - get_check_response_from_data(data, graph_ref) + get_check_response_from_data(data, graph_ref, subgraph) } fn get_check_response_from_data( data: MutationResponseData, graph_ref: GraphRef, + subgraph: String, ) -> Result { let service = data.service.ok_or(RoverClientError::GraphNotFound { graph_ref: graph_ref.clone(), @@ -75,17 +77,13 @@ fn get_check_response_from_data( let diff_to_previous = check_schema_result.diff_to_previous; - let number_of_checked_operations = - diff_to_previous.number_of_checked_operations.unwrap_or(0); + let operation_check_count = + diff_to_previous.number_of_checked_operations.unwrap_or(0) as u64; - let change_severity = diff_to_previous.severity.into(); + let result = diff_to_previous.severity.into(); let mut changes = Vec::with_capacity(diff_to_previous.changes.len()); - let mut num_failures = 0; for change in diff_to_previous.changes { - if let MutationChangeSeverity::FAILURE = change.severity { - num_failures += 1; - } changes.push(SchemaChange { code: change.code, severity: change.severity.into(), @@ -93,27 +91,27 @@ fn get_check_response_from_data( }); } - let check_response = CheckResponse { - num_failures, - target_url: check_schema_result.target_url, - number_of_checked_operations, + CheckResponse::try_new( + check_schema_result.target_url, + operation_check_count, changes, - change_severity, - }; - check_response.check_for_failures(graph_ref) + result, + graph_ref, + ) } else { let num_failures = query_composition_errors.len(); - let mut composition_errors = Vec::with_capacity(num_failures); + let mut build_errors = Vec::with_capacity(num_failures); for query_composition_error in query_composition_errors { - composition_errors.push(CompositionError { - message: query_composition_error.message, - code: query_composition_error.code, - }); + build_errors.push(BuildError::composition_error( + query_composition_error.message, + query_composition_error.code, + )); } - Err(RoverClientError::SubgraphCompositionErrors { + Err(RoverClientError::SubgraphBuildErrors { + subgraph, graph_ref, - composition_errors, + source: build_errors.into(), }) } } diff --git a/crates/rover-client/src/operations/subgraph/delete/runner.rs b/crates/rover-client/src/operations/subgraph/delete/runner.rs index a40e71d20..9acfd9ed4 100644 --- a/crates/rover-client/src/operations/subgraph/delete/runner.rs +++ b/crates/rover-client/src/operations/subgraph/delete/runner.rs @@ -1,6 +1,6 @@ use crate::blocking::StudioClient; use crate::operations::subgraph::delete::types::*; -use crate::shared::{CompositionError, GraphRef}; +use crate::shared::{BuildError, BuildErrors, GraphRef}; use crate::RoverClientError; use graphql_client::*; @@ -43,27 +43,19 @@ fn get_delete_data_from_response( } fn build_response(response: MutationComposition) -> SubgraphDeleteResponse { - let composition_errors: Vec = response + let build_errors: BuildErrors = response .errors .iter() .filter_map(|error| { - error.as_ref().map(|e| CompositionError { - message: e.message.clone(), - code: e.code.clone(), - }) + error + .as_ref() + .map(|e| BuildError::composition_error(e.message.clone(), e.code.clone())) }) .collect(); - // if there are no errors, just return None - let composition_errors = if !composition_errors.is_empty() { - Some(composition_errors) - } else { - None - }; - SubgraphDeleteResponse { - updated_gateway: response.updated_gateway, - composition_errors, + supergraph_was_updated: response.updated_gateway, + build_errors, } } @@ -136,17 +128,12 @@ mod tests { assert_eq!( parsed, SubgraphDeleteResponse { - composition_errors: Some(vec![ - CompositionError { - message: "wow".to_string(), - code: None - }, - CompositionError { - message: "boo".to_string(), - code: Some("BOO".to_string()) - } - ]), - updated_gateway: false, + build_errors: vec![ + BuildError::composition_error("wow".to_string(), None), + BuildError::composition_error("boo".to_string(), Some("BOO".to_string())) + ] + .into(), + supergraph_was_updated: false, } ); } @@ -162,8 +149,8 @@ mod tests { assert_eq!( parsed, SubgraphDeleteResponse { - composition_errors: None, - updated_gateway: true, + build_errors: BuildErrors::new(), + supergraph_was_updated: true, } ); } diff --git a/crates/rover-client/src/operations/subgraph/delete/types.rs b/crates/rover-client/src/operations/subgraph/delete/types.rs index 0bb7b67d2..86052896e 100644 --- a/crates/rover-client/src/operations/subgraph/delete/types.rs +++ b/crates/rover-client/src/operations/subgraph/delete/types.rs @@ -1,11 +1,13 @@ use crate::{ operations::subgraph::delete::runner::subgraph_delete_mutation, - shared::{CompositionError, GraphRef}, + shared::{BuildErrors, GraphRef}, }; pub(crate) type MutationComposition = subgraph_delete_mutation::SubgraphDeleteMutationServiceRemoveImplementingServiceAndTriggerComposition; pub(crate) type MutationVariables = subgraph_delete_mutation::Variables; +use serde::Serialize; + #[cfg(test)] pub(crate) type MutationCompositionErrors = subgraph_delete_mutation::SubgraphDeleteMutationServiceRemoveImplementingServiceAndTriggerCompositionErrors; @@ -19,11 +21,13 @@ pub struct SubgraphDeleteInput { /// this struct contains all the info needed to print the result of the delete. /// `updated_gateway` is true when composition succeeds and the gateway config /// is updated for the gateway to consume. `composition_errors` is just a list -/// of strings for when there are composition errors as a result of the delete. -#[derive(Debug, PartialEq)] +/// of strings for when there are build errors as a result of the delete. +#[derive(Debug, Clone, Serialize, PartialEq)] pub struct SubgraphDeleteResponse { - pub updated_gateway: bool, - pub composition_errors: Option>, + pub supergraph_was_updated: bool, + + #[serde(skip_serializing)] + pub build_errors: BuildErrors, } impl From for MutationVariables { diff --git a/crates/rover-client/src/operations/subgraph/list/mod.rs b/crates/rover-client/src/operations/subgraph/list/mod.rs index 912e0a83c..dd493dd4c 100644 --- a/crates/rover-client/src/operations/subgraph/list/mod.rs +++ b/crates/rover-client/src/operations/subgraph/list/mod.rs @@ -2,4 +2,4 @@ mod runner; mod types; pub use runner::run; -pub use types::{SubgraphListInput, SubgraphListResponse}; +pub use types::{SubgraphInfo, SubgraphListInput, SubgraphListResponse, SubgraphUpdatedAt}; diff --git a/crates/rover-client/src/operations/subgraph/list/runner.rs b/crates/rover-client/src/operations/subgraph/list/runner.rs index 1a5a2c3f6..2b9912783 100644 --- a/crates/rover-client/src/operations/subgraph/list/runner.rs +++ b/crates/rover-client/src/operations/subgraph/list/runner.rs @@ -78,14 +78,17 @@ fn format_subgraphs(subgraphs: &[QuerySubgraphInfo]) -> Vec { .map(|subgraph| SubgraphInfo { name: subgraph.name.clone(), url: subgraph.url.clone(), - updated_at: subgraph.updated_at.clone().parse().ok(), + updated_at: SubgraphUpdatedAt { + local: subgraph.updated_at.clone().parse().ok(), + utc: subgraph.updated_at.clone().parse().ok(), + }, }) .collect(); // sort and reverse, so newer items come first. We use _unstable here, since // we don't care which order equal items come in the list (it's unlikely that // we'll even have equal items after all) - subgraphs.sort_unstable_by(|a, b| a.updated_at.cmp(&b.updated_at).reverse()); + subgraphs.sort_unstable_by(|a, b| a.updated_at.utc.cmp(&b.updated_at.utc).reverse()); subgraphs } diff --git a/crates/rover-client/src/operations/subgraph/list/types.rs b/crates/rover-client/src/operations/subgraph/list/types.rs index af260a33d..8e18dd9f2 100644 --- a/crates/rover-client/src/operations/subgraph/list/types.rs +++ b/crates/rover-client/src/operations/subgraph/list/types.rs @@ -6,7 +6,8 @@ pub(crate) type QueryGraphType = subgraph_list_query::SubgraphListQueryServiceIm type QueryVariables = subgraph_list_query::Variables; -use chrono::{DateTime, Local}; +use chrono::{DateTime, Local, Utc}; +use serde::Serialize; #[derive(Clone, PartialEq, Debug)] pub struct SubgraphListInput { @@ -22,16 +23,26 @@ impl From for QueryVariables { } } -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Serialize, PartialEq, Debug)] pub struct SubgraphListResponse { pub subgraphs: Vec, + + #[serde(skip_serializing)] pub root_url: String, + + #[serde(skip_serializing)] pub graph_ref: GraphRef, } -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Serialize, PartialEq, Debug)] pub struct SubgraphInfo { pub name: String, pub url: Option, // optional, and may not be a real url - pub updated_at: Option>, + pub updated_at: SubgraphUpdatedAt, +} + +#[derive(Clone, Serialize, PartialEq, Debug)] +pub struct SubgraphUpdatedAt { + pub local: Option>, + pub utc: Option>, } diff --git a/crates/rover-client/src/operations/subgraph/publish/publish_mutation.graphql b/crates/rover-client/src/operations/subgraph/publish/publish_mutation.graphql index 06fade4d7..0777dadd5 100644 --- a/crates/rover-client/src/operations/subgraph/publish/publish_mutation.graphql +++ b/crates/rover-client/src/operations/subgraph/publish/publish_mutation.graphql @@ -5,7 +5,7 @@ mutation SubgraphPublishMutation( $url: String $revision: String! $schema: PartialSchemaInput! - $gitContext: GitContextInput! + $git_context: GitContextInput! ) { service(id: $graph_id) { upsertImplementingServiceAndTriggerComposition( @@ -14,7 +14,7 @@ mutation SubgraphPublishMutation( revision: $revision activePartialSchema: $schema graphVariant: $variant - gitContext: $gitContext + gitContext: $git_context ) { compositionConfig { schemaHash diff --git a/crates/rover-client/src/operations/subgraph/publish/runner.rs b/crates/rover-client/src/operations/subgraph/publish/runner.rs index fbd1c3aa2..9f6df852b 100644 --- a/crates/rover-client/src/operations/subgraph/publish/runner.rs +++ b/crates/rover-client/src/operations/subgraph/publish/runner.rs @@ -1,7 +1,7 @@ use super::types::*; use crate::blocking::StudioClient; use crate::operations::config::is_federated::{self, IsFederatedInput}; -use crate::shared::{CompositionError, GraphRef}; +use crate::shared::{BuildError, BuildErrors, GraphRef}; use crate::RoverClientError; use graphql_client::*; @@ -60,32 +60,24 @@ fn get_publish_response_from_data( } fn build_response(publish_response: UpdateResponse) -> SubgraphPublishResponse { - let composition_errors: Vec = publish_response + let build_errors: BuildErrors = publish_response .errors .iter() .filter_map(|error| { - error.as_ref().map(|e| CompositionError { - message: e.message.clone(), - code: e.code.clone(), - }) + error + .as_ref() + .map(|e| BuildError::composition_error(e.message.clone(), e.code.clone())) }) .collect(); - // if there are no errors, just return None - let composition_errors = if !composition_errors.is_empty() { - Some(composition_errors) - } else { - None - }; - SubgraphPublishResponse { - schema_hash: match publish_response.composition_config { + api_schema_hash: match publish_response.composition_config { Some(config) => Some(config.schema_hash), None => None, }, - did_update_gateway: publish_response.did_update_gateway, + supergraph_was_updated: publish_response.did_update_gateway, subgraph_was_created: publish_response.service_was_created, - composition_errors, + build_errors, } } @@ -99,7 +91,7 @@ mod tests { "compositionConfig": { "schemaHash": "5gf564" }, "errors": [ { - "message": "[Accounts] User -> composition error", + "message": "[Accounts] User -> build error", "code": null }, null, // this is technically allowed in the types @@ -117,18 +109,19 @@ mod tests { assert_eq!( output, SubgraphPublishResponse { - schema_hash: Some("5gf564".to_string()), - composition_errors: Some(vec![ - CompositionError { - message: "[Accounts] User -> composition error".to_string(), - code: None - }, - CompositionError { - message: "[Products] Product -> another one".to_string(), - code: Some("ERROR".to_string()) - } - ]), - did_update_gateway: false, + api_schema_hash: Some("5gf564".to_string()), + build_errors: vec![ + BuildError::composition_error( + "[Accounts] User -> build error".to_string(), + None + ), + BuildError::composition_error( + "[Products] Product -> another one".to_string(), + Some("ERROR".to_string()) + ) + ] + .into(), + supergraph_was_updated: false, subgraph_was_created: true, } ); @@ -148,9 +141,9 @@ mod tests { assert_eq!( output, SubgraphPublishResponse { - schema_hash: Some("5gf564".to_string()), - composition_errors: None, - did_update_gateway: true, + api_schema_hash: Some("5gf564".to_string()), + build_errors: BuildErrors::new(), + supergraph_was_updated: true, subgraph_was_created: true, } ); @@ -175,12 +168,13 @@ mod tests { assert_eq!( output, SubgraphPublishResponse { - schema_hash: None, - composition_errors: Some(vec![CompositionError { - message: "[Accounts] -> Things went really wrong".to_string(), - code: None - }]), - did_update_gateway: false, + api_schema_hash: None, + build_errors: vec![BuildError::composition_error( + "[Accounts] -> Things went really wrong".to_string(), + None + )] + .into(), + supergraph_was_updated: false, subgraph_was_created: false, } ); diff --git a/crates/rover-client/src/operations/subgraph/publish/types.rs b/crates/rover-client/src/operations/subgraph/publish/types.rs index 6472a70e4..4dd7eb182 100644 --- a/crates/rover-client/src/operations/subgraph/publish/types.rs +++ b/crates/rover-client/src/operations/subgraph/publish/types.rs @@ -1,6 +1,6 @@ use super::runner::subgraph_publish_mutation; -use crate::shared::{CompositionError, GitContext, GraphRef}; +use crate::shared::{BuildErrors, GitContext, GraphRef}; pub(crate) type ResponseData = subgraph_publish_mutation::ResponseData; pub(crate) type MutationVariables = subgraph_publish_mutation::Variables; @@ -9,6 +9,8 @@ pub(crate) type UpdateResponse = subgraph_publish_mutation::SubgraphPublishMutat type SchemaInput = subgraph_publish_mutation::PartialSchemaInput; type GitContextInput = subgraph_publish_mutation::GitContextInput; +use serde::Serialize; + #[derive(Debug, Clone, PartialEq)] pub struct SubgraphPublishInput { pub graph_ref: GraphRef, @@ -19,12 +21,16 @@ pub struct SubgraphPublishInput { pub convert_to_federated_graph: bool, } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, Serialize, PartialEq)] pub struct SubgraphPublishResponse { - pub schema_hash: Option, - pub did_update_gateway: bool, + pub api_schema_hash: Option, + + pub supergraph_was_updated: bool, + pub subgraph_was_created: bool, - pub composition_errors: Option>, + + #[serde(skip_serializing)] + pub build_errors: BuildErrors, } impl From for MutationVariables { diff --git a/crates/rover-client/src/operations/supergraph/fetch/runner.rs b/crates/rover-client/src/operations/supergraph/fetch/runner.rs index e0fc3d980..8374c6d51 100644 --- a/crates/rover-client/src/operations/supergraph/fetch/runner.rs +++ b/crates/rover-client/src/operations/supergraph/fetch/runner.rs @@ -1,6 +1,6 @@ use crate::blocking::StudioClient; use crate::operations::supergraph::fetch::SupergraphFetchInput; -use crate::shared::{CompositionError, FetchResponse, GraphRef, Sdl, SdlType}; +use crate::shared::{BuildError, BuildErrors, FetchResponse, GraphRef, Sdl, SdlType}; use crate::RoverClientError; use graphql_client::*; @@ -67,17 +67,14 @@ fn get_supergraph_sdl_from_response_data( } else if let Some(most_recent_composition_publish) = service_data.most_recent_composition_publish { - let composition_errors = most_recent_composition_publish + let build_errors: BuildErrors = most_recent_composition_publish .errors .into_iter() - .map(|error| CompositionError { - message: error.message, - code: error.code, - }) + .map(|error| BuildError::composition_error(error.message, error.code)) .collect(); - Err(RoverClientError::NoCompositionPublishes { + Err(RoverClientError::NoSupergraphBuilds { graph_ref, - composition_errors, + source: build_errors, }) } else { let mut valid_variants = Vec::new(); @@ -154,24 +151,24 @@ mod tests { #[test] fn get_schema_from_response_data_errs_on_no_schema_tag() { - let composition_errors = vec![ - CompositionError { - message: "Unknown type \"Unicorn\".".to_string(), - code: Some("UNKNOWN_TYPE".to_string()), - }, - CompositionError { - message: "Type Query must define one or more fields.".to_string(), - code: None, - }, + let build_errors = vec![ + BuildError::composition_error( + "Unknown type \"Unicorn\".".to_string(), + Some("UNKNOWN_TYPE".to_string()), + ), + BuildError::composition_error( + "Type Query must define one or more fields.".to_string(), + None, + ), ]; - let composition_errors_json = json!([ + let build_errors_json = json!([ { - "message": composition_errors[0].message, - "code": composition_errors[0].code + "message": build_errors[0].get_message(), + "code": build_errors[0].get_code() }, { - "message": composition_errors[1].message, - "code": composition_errors[1].code + "message": build_errors[1].get_message(), + "code": build_errors[1].get_code() } ]); let graph_ref = mock_graph_ref(); @@ -181,16 +178,16 @@ mod tests { "schemaTag": null, "variants": [{"name": &graph_ref.variant}], "mostRecentCompositionPublish": { - "errors": composition_errors_json, + "errors": build_errors_json, } }, }); let data: supergraph_fetch_query::ResponseData = serde_json::from_value(json_response).unwrap(); let output = get_supergraph_sdl_from_response_data(data, graph_ref.clone()); - let expected_error = RoverClientError::NoCompositionPublishes { + let expected_error = RoverClientError::NoSupergraphBuilds { graph_ref, - composition_errors, + source: build_errors.into(), } .to_string(); let actual_error = output.unwrap_err().to_string(); diff --git a/crates/rover-client/src/shared/build_errors.rs b/crates/rover-client/src/shared/build_errors.rs new file mode 100644 index 000000000..1e7e66c8c --- /dev/null +++ b/crates/rover-client/src/shared/build_errors.rs @@ -0,0 +1,174 @@ +use std::{ + error::Error, + fmt::{self, Display}, + iter::FromIterator, +}; + +use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct BuildError { + message: String, + code: Option, + r#type: BuildErrorType, +} + +impl BuildError { + pub fn composition_error(message: String, code: Option) -> BuildError { + BuildError { + message, + code, + r#type: BuildErrorType::Composition, + } + } + + pub fn get_message(&self) -> String { + self.message.clone() + } + + pub fn get_code(&self) -> Option { + self.code.clone() + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum BuildErrorType { + Composition, +} + +impl Display for BuildError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(code) = &self.code { + write!(f, "{}: ", code)?; + } else { + write!(f, "UNKNOWN: ")?; + } + write!(f, "{}", &self.message) + } +} + +#[derive(Debug, Deserialize, Default, Clone, PartialEq)] +pub struct BuildErrors { + build_errors: Vec, +} + +impl Serialize for BuildErrors { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut sequence = serializer.serialize_seq(Some(self.build_errors.len()))?; + for build_error in &self.build_errors { + sequence.serialize_element(build_error)?; + } + sequence.end() + } +} + +impl BuildErrors { + pub fn new() -> Self { + BuildErrors { + build_errors: Vec::new(), + } + } + + pub fn len(&self) -> usize { + self.build_errors.len() + } + + pub fn length_string(&self) -> String { + let num_failures = self.build_errors.len(); + if num_failures == 0 { + unreachable!("No build errors were encountered while composing the supergraph."); + } + + match num_failures { + 1 => "1 build error".to_string(), + _ => format!("{} build errors", num_failures), + } + } + + pub fn push(&mut self, error: BuildError) { + self.build_errors.push(error); + } + + pub fn is_empty(&self) -> bool { + self.build_errors.is_empty() + } +} + +impl Display for BuildErrors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for build_error in &self.build_errors { + writeln!(f, "{}", build_error)?; + } + Ok(()) + } +} + +impl From> for BuildErrors { + fn from(build_errors: Vec) -> Self { + BuildErrors { build_errors } + } +} + +impl FromIterator for BuildErrors { + fn from_iter>(iter: I) -> Self { + let mut c = BuildErrors::new(); + + for i in iter { + c.push(i); + } + + c + } +} + +impl Error for BuildError {} +impl Error for BuildErrors {} + +#[cfg(test)] +mod tests { + use super::{BuildError, BuildErrors}; + + use serde_json::{json, Value}; + + #[test] + fn it_can_serialize_empty_errors() { + let build_errors = BuildErrors::new(); + assert_eq!( + serde_json::to_string(&build_errors).expect("Could not serialize build errors"), + json!([]).to_string() + ); + } + + #[test] + fn it_can_serialize_some_build_errors() { + let build_errors: BuildErrors = vec![ + BuildError::composition_error("wow".to_string(), None), + BuildError::composition_error("boo".to_string(), Some("BOO".to_string())), + ] + .into(); + + let actual_value: Value = serde_json::from_str( + &serde_json::to_string(&build_errors) + .expect("Could not convert build errors to string"), + ) + .expect("Could not convert build error string to serde_json::Value"); + + let expected_value = json!([ + { + "code": null, + "message": "wow", + "type": "composition" + }, + { + "code": "BOO", + "message": "boo", + "type": "composition" + } + ]); + assert_eq!(actual_value, expected_value); + } +} diff --git a/crates/rover-client/src/shared/check_response.rs b/crates/rover-client/src/shared/check_response.rs index 135bda99c..2831ab25a 100644 --- a/crates/rover-client/src/shared/check_response.rs +++ b/crates/rover-client/src/shared/check_response.rs @@ -1,64 +1,107 @@ use std::cmp::Ordering; -use std::fmt; +use std::fmt::{self}; use std::str::FromStr; use crate::shared::GraphRef; use crate::RoverClientError; +use prettytable::format::consts::FORMAT_BOX_CHARS; use serde::Serialize; +use prettytable::{cell, row, Table}; +use serde_json::{json, Value}; + /// CheckResponse is the return type of the /// `graph` and `subgraph` check operations -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] pub struct CheckResponse { - pub target_url: Option, - pub number_of_checked_operations: i64, - pub changes: Vec, - pub change_severity: ChangeSeverity, - pub num_failures: i64, + target_url: Option, + operation_check_count: u64, + changes: Vec, + #[serde(skip_serializing)] + result: ChangeSeverity, + failure_count: u64, } impl CheckResponse { - pub fn new( + pub fn try_new( target_url: Option, - number_of_checked_operations: i64, + operation_check_count: u64, changes: Vec, - change_severity: ChangeSeverity, - ) -> CheckResponse { - let mut num_failures = 0; + result: ChangeSeverity, + graph_ref: GraphRef, + ) -> Result { + let mut failure_count = 0; for change in &changes { if let ChangeSeverity::FAIL = change.severity { - num_failures += 1; + failure_count += 1; } } - CheckResponse { + let check_response = CheckResponse { target_url, - number_of_checked_operations, + operation_check_count, changes, - change_severity, - num_failures, - } - } + result, + failure_count, + }; - pub fn check_for_failures( - &self, - graph_ref: GraphRef, - ) -> Result { - match &self.num_failures.cmp(&0) { - Ordering::Equal => Ok(self.clone()), + match failure_count.cmp(&0) { + Ordering::Equal => Ok(check_response), Ordering::Greater => Err(RoverClientError::OperationCheckFailure { graph_ref, - check_response: self.clone(), + check_response, }), Ordering::Less => unreachable!("Somehow encountered a negative number of failures."), } } + + pub fn get_table(&self) -> String { + let num_changes = self.changes.len(); + + let mut msg = match num_changes { + 0 => "There were no changes detected in the composed schema.".to_string(), + _ => format!( + "Compared {} schema changes against {} operations", + num_changes, self.operation_check_count + ), + }; + + msg.push('\n'); + + if !self.changes.is_empty() { + let mut table = Table::new(); + + table.set_format(*FORMAT_BOX_CHARS); + + // bc => sets top row to be bold and center + table.add_row(row![bc => "Change", "Code", "Description"]); + for check in &self.changes { + table.add_row(row![check.severity, check.code, check.description]); + } + + msg.push_str(&table.to_string()); + } + + if let Some(url) = &self.target_url { + msg.push_str(&format!("View full details at {}", url)); + } + + msg + } + + pub fn get_failure_count(&self) -> u64 { + self.failure_count + } + + pub fn get_json(&self) -> Value { + json!(self) + } } /// ChangeSeverity indicates whether a proposed change /// in a GraphQL schema passed or failed the check -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] pub enum ChangeSeverity { /// The proposed schema has passed the checks PASS, @@ -89,7 +132,7 @@ impl fmt::Display for ChangeSeverity { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] pub struct SchemaChange { /// The code associated with a given change /// e.g. 'TYPE_REMOVED' diff --git a/crates/rover-client/src/shared/composition_error.rs b/crates/rover-client/src/shared/composition_error.rs deleted file mode 100644 index b45e51c5a..000000000 --- a/crates/rover-client/src/shared/composition_error.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::fmt::{self, Display}; - -#[derive(Debug, Clone, PartialEq)] -pub struct CompositionError { - pub message: String, - pub code: Option, -} - -impl Display for CompositionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(code) = &self.code { - write!(f, "{}: ", code)?; - } else { - write!(f, "UNKNOWN: ")?; - } - write!(f, "{}", &self.message) - } -} diff --git a/crates/rover-client/src/shared/fetch_response.rs b/crates/rover-client/src/shared/fetch_response.rs index 5f4ab4381..b5ddc80bb 100644 --- a/crates/rover-client/src/shared/fetch_response.rs +++ b/crates/rover-client/src/shared/fetch_response.rs @@ -1,15 +1,19 @@ -#[derive(Debug, Clone, PartialEq)] +use serde::Serialize; + +#[derive(Debug, Clone, Serialize, PartialEq)] pub struct FetchResponse { pub sdl: Sdl, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Serialize, PartialEq)] pub struct Sdl { pub contents: String, + #[serde(skip_serializing)] pub r#type: SdlType, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all(serialize = "lowercase"))] pub enum SdlType { Graph, Subgraph, diff --git a/crates/rover-client/src/shared/graph_ref.rs b/crates/rover-client/src/shared/graph_ref.rs index 5cad87c3e..99d0c16f1 100644 --- a/crates/rover-client/src/shared/graph_ref.rs +++ b/crates/rover-client/src/shared/graph_ref.rs @@ -4,8 +4,9 @@ use std::str::FromStr; use crate::RoverClientError; use regex::Regex; +use serde::Serialize; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] pub struct GraphRef { pub name: String, pub variant: String, diff --git a/crates/rover-client/src/shared/mod.rs b/crates/rover-client/src/shared/mod.rs index 31e0f3e97..e09603fb1 100644 --- a/crates/rover-client/src/shared/mod.rs +++ b/crates/rover-client/src/shared/mod.rs @@ -1,13 +1,13 @@ +mod build_errors; mod check_response; -mod composition_error; mod fetch_response; mod git_context; mod graph_ref; +pub use build_errors::{BuildError, BuildErrors}; pub use check_response::{ ChangeSeverity, CheckConfig, CheckResponse, SchemaChange, ValidationPeriod, }; -pub use composition_error::CompositionError; pub use fetch_response::{FetchResponse, Sdl, SdlType}; pub use git_context::GitContext; pub use graph_ref::GraphRef; diff --git a/crates/sdl-encoder/src/schema_def.rs b/crates/sdl-encoder/src/schema_def.rs index 19905380e..2d74b7c45 100644 --- a/crates/sdl-encoder/src/schema_def.rs +++ b/crates/sdl-encoder/src/schema_def.rs @@ -81,7 +81,7 @@ impl Default for SchemaDef { impl Display for SchemaDef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(description) = &self.description { - // We are determing on whether to have description formatted as + // We determine whether to have description formatted as // a multiline comment based on whether or not it already includes a // \n. match description.contains('\n') { diff --git a/docs/source/errors.md b/docs/source/errors.md index cc2a8f55f..0e258a69a 100644 --- a/docs/source/errors.md +++ b/docs/source/errors.md @@ -237,9 +237,9 @@ If you encountered this error while running introspection, you'll want to make s This error occurs when you propose a subgraph schema that could not be composed. -There are many reasons why you may run into composition errors. This error should include information about _why_ the proposed subgraph schema could not be composed. Error code references can be found [here](https://www.apollographql.com/docs/federation/errors/). +There are many reasons why you may run into build errors. This error should include information about _why_ the proposed subgraph schema could not be composed. Error code references can be found [here](https://www.apollographql.com/docs/federation/errors/). -Some composition errors are part of normal workflows. For instance, you may need to publish a subgraph that does not compose if you are trying to [migrate an entity or field](https://www.apollographql.com/docs/federation/entities/#migrating-entities-and-fields-advanced). +Some build errors are part of normal workflows. For instance, you may need to publish a subgraph that does not compose if you are trying to [migrate an entity or field](https://www.apollographql.com/docs/federation/entities/#migrating-entities-and-fields-advanced). ### E030 diff --git a/src/bin/rover.rs b/src/bin/rover.rs index cb663447c..25ce697d0 100644 --- a/src/bin/rover.rs +++ b/src/bin/rover.rs @@ -1,65 +1,16 @@ -use command::RoverStdout; use robot_panic::setup_panic; -use rover::*; -use sputnik::Session; +use rover::cli::Rover; use structopt::StructOpt; -use std::{process, thread}; - fn main() { setup_panic!(Metadata { - name: PKG_NAME.into(), - version: PKG_VERSION.into(), - authors: PKG_AUTHORS.into(), - homepage: PKG_HOMEPAGE.into(), - repository: PKG_REPOSITORY.into() + name: rover::PKG_NAME.into(), + version: rover::PKG_VERSION.into(), + authors: rover::PKG_AUTHORS.into(), + homepage: rover::PKG_HOMEPAGE.into(), + repository: rover::PKG_REPOSITORY.into() }); - if let Err(error) = run() { - tracing::debug!(?error); - eprint!("{}", 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 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 - let report_thread = thread::spawn(move || { - // log + ignore errors because it is not in the critical path - let _ = session.report().map_err(|telemetry_error| { - tracing::debug!(?telemetry_error); - telemetry_error - }); - }); - - // kicks off the app on the main thread - // don't return an error with ? quite yet - // since we still want to report the usage data - let app_result = app.run(); - - // makes sure the reporting finishes in the background - // before continuing. - // ignore errors because it is not in the critical path - let _ = report_thread.join(); - - // return result of app execution - // now that we have reported our usage data - app_result - } - - // otherwise just run the app without reporting - Err(_) => app.run(), - }?; - output.print(); - Ok(()) + let app = Rover::from_args(); + app.run(); } diff --git a/src/cli.rs b/src/cli.rs index 67224b47c..157548841 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,21 +1,25 @@ +use camino::Utf8PathBuf; use reqwest::blocking::Client; use serde::Serialize; use structopt::{clap::AppSettings, StructOpt}; -use crate::command::{self, RoverStdout}; +use crate::command::output::JsonOutput; +use crate::command::{self, RoverOutput}; use crate::utils::{ client::StudioClientConfig, env::{RoverEnv, RoverEnvKey}, - stringify::from_display, + stringify::option_from_display, version, }; -use crate::Result; +use crate::{anyhow, Result}; + use config::Config; use houston as config; use rover_client::shared::GitContext; +use sputnik::Session; use timber::{Level, LEVELS}; -use camino::Utf8PathBuf; +use std::{process, str::FromStr, thread}; #[derive(Debug, Serialize, StructOpt)] #[structopt( @@ -51,16 +55,20 @@ You can open the full documentation for Rover by running: ")] pub struct Rover { #[structopt(subcommand)] - pub command: Command, + command: Command, /// Specify Rover's log level #[structopt(long = "log", short = "l", global = true, possible_values = &LEVELS, case_insensitive = true)] - #[serde(serialize_with = "from_display")] - pub log_level: Option, + #[serde(serialize_with = "option_from_display")] + log_level: Option, + + /// Use json output + #[structopt(long = "output", default_value = "plain", possible_values = &["json", "plain"], case_insensitive = true, global = true)] + output_type: OutputType, #[structopt(skip)] #[serde(skip_serializing)] - pub env_store: RoverEnv, + pub(crate) env_store: RoverEnv, #[structopt(skip)] #[serde(skip_serializing)] @@ -68,6 +76,96 @@ pub struct Rover { } impl Rover { + pub fn run(&self) -> ! { + timber::init(self.log_level); + tracing::trace!(command_structure = ?self); + + // attempt to create a new `Session` to capture anonymous usage data + let rover_output = match Session::new(self) { + // if successful, report the usage data in the background + Ok(session) => { + // kicks off the reporting on a background thread + let report_thread = thread::spawn(move || { + // log + ignore errors because it is not in the critical path + let _ = session.report().map_err(|telemetry_error| { + tracing::debug!(?telemetry_error); + telemetry_error + }); + }); + + // kicks off the app on the main thread + // don't return an error with ? quite yet + // since we still want to report the usage data + let app_result = self.execute_command(); + + // makes sure the reporting finishes in the background + // before continuing. + // ignore errors because it is not in the critical path + let _ = report_thread.join(); + + // return result of app execution + // now that we have reported our usage data + app_result + } + + // otherwise just run the app without reporting + Err(_) => self.execute_command(), + }; + + match rover_output { + Ok(output) => { + match self.output_type { + OutputType::Plain => output.print(), + OutputType::Json => println!("{}", JsonOutput::from(output)), + } + process::exit(0); + } + Err(error) => { + match self.output_type { + OutputType::Json => println!("{}", JsonOutput::from(error)), + OutputType::Plain => { + tracing::debug!(?error); + error.print(); + } + } + process::exit(1); + } + } + } + + pub fn execute_command(&self) -> Result { + // before running any commands, we check if rover is up to date + // this only happens once a day automatically + // we skip this check for the `rover update` commands, since they + // do their own checks + + if let Command::Update(_) = &self.command { /* skip check */ + } else { + let config = self.get_rover_config(); + if let Ok(config) = config { + let _ = version::check_for_update(config, false, self.get_reqwest_client()); + } + } + + match &self.command { + Command::Config(command) => command.run(self.get_client_config()?), + Command::Supergraph(command) => command.run(self.get_client_config()?), + Command::Docs(command) => command.run(), + Command::Graph(command) => { + command.run(self.get_client_config()?, self.get_git_context()?) + } + Command::Subgraph(command) => { + command.run(self.get_client_config()?, self.get_git_context()?) + } + Command::Update(command) => { + command.run(self.get_rover_config()?, self.get_reqwest_client()) + } + Command::Install(command) => command.run(self.get_install_override_path()?), + Command::Info(command) => command.run(), + Command::Explain(command) => command.run(), + } + } + pub(crate) fn get_rover_config(&self) -> Result { let override_home: Option = self .env_store @@ -146,37 +244,26 @@ pub enum Command { Explain(command::Explain), } -impl Rover { - pub fn run(&self) -> Result { - // before running any commands, we check if rover is up to date - // this only happens once a day automatically - // we skip this check for the `rover update` commands, since they - // do their own checks +#[derive(Debug, Serialize, Clone, PartialEq)] +pub enum OutputType { + Plain, + Json, +} - if let Command::Update(_) = &self.command { /* skip check */ - } else { - let config = self.get_rover_config(); - if let Ok(config) = config { - let _ = version::check_for_update(config, false, self.get_reqwest_client()); - } - } +impl FromStr for OutputType { + type Err = anyhow::Error; - match &self.command { - Command::Config(command) => command.run(self.get_client_config()?), - Command::Supergraph(command) => command.run(self.get_client_config()?), - Command::Docs(command) => command.run(), - Command::Graph(command) => { - command.run(self.get_client_config()?, self.get_git_context()?) - } - Command::Subgraph(command) => { - command.run(self.get_client_config()?, self.get_git_context()?) - } - Command::Update(command) => { - command.run(self.get_rover_config()?, self.get_reqwest_client()) - } - Command::Install(command) => command.run(self.get_install_override_path()?), - Command::Info(command) => command.run(), - Command::Explain(command) => command.run(), + fn from_str(input: &str) -> std::result::Result { + match input { + "plain" => Ok(Self::Plain), + "json" => Ok(Self::Json), + _ => Err(anyhow!("Invalid output type.")), } } } + +impl Default for OutputType { + fn default() -> Self { + OutputType::Plain + } +} diff --git a/src/command/config/auth.rs b/src/command/config/auth.rs index ae15cb16f..90951bcec 100644 --- a/src/command/config/auth.rs +++ b/src/command/config/auth.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use config::Profile; use houston as config; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::{anyhow, Result}; #[derive(Debug, Serialize, StructOpt)] @@ -26,13 +26,13 @@ pub struct Auth { } impl Auth { - pub fn run(&self, config: config::Config) -> Result { + pub fn run(&self, config: config::Config) -> Result { let api_key = api_key_prompt()?; Profile::set_api_key(&self.profile_name, &config, &api_key)?; Profile::get_credential(&self.profile_name, &config).map(|_| { eprintln!("Successfully saved API key."); })?; - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/config/clear.rs b/src/command/config/clear.rs index a587885cb..5ebc9f214 100644 --- a/src/command/config/clear.rs +++ b/src/command/config/clear.rs @@ -1,7 +1,7 @@ use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; use houston as config; @@ -13,9 +13,9 @@ use houston as config; pub struct Clear {} impl Clear { - pub fn run(&self, config: config::Config) -> Result { + pub fn run(&self, config: config::Config) -> Result { config.clear()?; eprintln!("Successfully cleared all configuration."); - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/config/delete.rs b/src/command/config/delete.rs index 9c9664b4f..e282ae0bb 100644 --- a/src/command/config/delete.rs +++ b/src/command/config/delete.rs @@ -3,7 +3,7 @@ use structopt::StructOpt; use houston as config; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; #[derive(Debug, Serialize, StructOpt)] @@ -20,9 +20,9 @@ pub struct Delete { } impl Delete { - pub fn run(&self, config: config::Config) -> Result { + pub fn run(&self, config: config::Config) -> Result { config::Profile::delete(&self.name, &config)?; eprintln!("Successfully deleted profile \"{}\"", &self.name); - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/config/list.rs b/src/command/config/list.rs index 907bc0f9c..9bd56b892 100644 --- a/src/command/config/list.rs +++ b/src/command/config/list.rs @@ -4,15 +4,15 @@ use structopt::StructOpt; use crate::Result; use houston as config; -use crate::command::RoverStdout; +use crate::command::RoverOutput; #[derive(Serialize, Debug, StructOpt)] /// List all configuration profiles pub struct List {} impl List { - pub fn run(&self, config: config::Config) -> Result { + pub fn run(&self, config: config::Config) -> Result { let profiles = config::Profile::list(&config)?; - Ok(RoverStdout::Profiles(profiles)) + Ok(RoverOutput::Profiles(profiles)) } } diff --git a/src/command/config/mod.rs b/src/command/config/mod.rs index a9eed151a..71a1b87aa 100644 --- a/src/command/config/mod.rs +++ b/src/command/config/mod.rs @@ -7,7 +7,7 @@ mod whoami; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -36,7 +36,7 @@ pub enum Command { } impl Config { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { match &self.command { Command::Auth(command) => command.run(client_config.config), Command::List(command) => command.run(client_config.config), diff --git a/src/command/config/whoami.rs b/src/command/config/whoami.rs index 7398f9ceb..c0d7f7d35 100644 --- a/src/command/config/whoami.rs +++ b/src/command/config/whoami.rs @@ -6,7 +6,7 @@ use structopt::StructOpt; use houston::CredentialOrigin; use crate::anyhow; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::utils::env::RoverEnvKey; use crate::Result; @@ -22,7 +22,7 @@ pub struct WhoAmI { } impl WhoAmI { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; eprintln!("Checking identity of your API key against the registry."); @@ -80,6 +80,6 @@ impl WhoAmI { eprintln!("{}", message); - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/docs/list.rs b/src/command/docs/list.rs index 6a1190f2b..3f951f57a 100644 --- a/src/command/docs/list.rs +++ b/src/command/docs/list.rs @@ -1,4 +1,4 @@ -use crate::{command::RoverStdout, Result}; +use crate::{command::RoverOutput, Result}; use super::shortlinks; @@ -9,8 +9,8 @@ use structopt::StructOpt; pub struct List {} impl List { - pub fn run(&self) -> Result { - Ok(RoverStdout::DocsList( + pub fn run(&self) -> Result { + Ok(RoverOutput::DocsList( shortlinks::get_shortlinks_with_description(), )) } diff --git a/src/command/docs/mod.rs b/src/command/docs/mod.rs index 0950c0eb4..ce3c0f6c8 100644 --- a/src/command/docs/mod.rs +++ b/src/command/docs/mod.rs @@ -5,7 +5,7 @@ pub mod shortlinks; use serde::Serialize; use structopt::StructOpt; -use crate::{command::RoverStdout, Result}; +use crate::{command::RoverOutput, Result}; #[derive(Debug, Serialize, StructOpt)] pub struct Docs { @@ -23,7 +23,7 @@ pub enum Command { } impl Docs { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { match &self.command { Command::List(command) => command.run(), Command::Open(command) => command.run(), diff --git a/src/command/docs/open.rs b/src/command/docs/open.rs index 7e43ff6db..8f39136d3 100644 --- a/src/command/docs/open.rs +++ b/src/command/docs/open.rs @@ -1,4 +1,4 @@ -use crate::{anyhow, command::RoverStdout, Result}; +use crate::{anyhow, command::RoverOutput, Result}; use super::shortlinks; @@ -15,7 +15,7 @@ pub struct Open { } impl Open { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { let url = shortlinks::get_url_from_slug(&self.slug); let yellow_browser_var = format!("{}", Yellow.normal().paint("$BROWSER")); let cyan_url = format!("{}", Cyan.normal().paint(&url)); @@ -40,6 +40,6 @@ impl Open { Ok(()) }?; - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/docs/shortlinks.rs b/src/command/docs/shortlinks.rs index 18833e758..6471824c0 100644 --- a/src/command/docs/shortlinks.rs +++ b/src/command/docs/shortlinks.rs @@ -1,9 +1,9 @@ pub const URL_BASE: &str = "https://go.apollo.dev/r"; -use std::collections::HashMap; +use std::collections::BTreeMap; -pub fn get_shortlinks_with_description() -> HashMap<&'static str, &'static str> { - let mut links = HashMap::new(); +pub fn get_shortlinks_with_description() -> BTreeMap<&'static str, &'static str> { + let mut links = BTreeMap::new(); links.insert("docs", "Rover's Documentation Homepage"); links.insert("api-keys", "Understanding Apollo's API Keys"); links.insert("contributing", "Contributing to Rover"); diff --git a/src/command/explain.rs b/src/command/explain.rs index 4c2d3a5ad..4de0510d7 100644 --- a/src/command/explain.rs +++ b/src/command/explain.rs @@ -1,4 +1,4 @@ -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::error::metadata::code::Code; use crate::Result; use serde::Serialize; @@ -12,8 +12,8 @@ pub struct Explain { } impl Explain { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { let explanation = &self.code.explain(); - Ok(RoverStdout::Markdown(explanation.clone())) + Ok(RoverOutput::ErrorExplanation(explanation.clone())) } } diff --git a/src/command/graph/check.rs b/src/command/graph/check.rs index 97035155b..d18851021 100644 --- a/src/command/graph/check.rs +++ b/src/command/graph/check.rs @@ -4,7 +4,7 @@ use structopt::StructOpt; use rover_client::operations::graph::check::{self, GraphCheckInput}; use rover_client::shared::{CheckConfig, GitContext, GraphRef, ValidationPeriod}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{ @@ -53,7 +53,7 @@ impl Check { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let proposed_schema = load_schema_from_flag(&self.schema, std::io::stdin())?; @@ -76,6 +76,6 @@ impl Check { &client, )?; - Ok(RoverStdout::CheckResponse(res)) + Ok(RoverOutput::CheckResponse(res)) } } diff --git a/src/command/graph/fetch.rs b/src/command/graph/fetch.rs index 2e01fc0f8..478811b2d 100644 --- a/src/command/graph/fetch.rs +++ b/src/command/graph/fetch.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use rover_client::operations::graph::fetch::{self, GraphFetchInput}; use rover_client::shared::GraphRef; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -24,7 +24,7 @@ pub struct Fetch { } impl Fetch { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -40,6 +40,6 @@ impl Fetch { &client, )?; - Ok(RoverStdout::FetchResponse(fetch_response)) + Ok(RoverOutput::FetchResponse(fetch_response)) } } diff --git a/src/command/graph/introspect.rs b/src/command/graph/introspect.rs index 991f09e5d..da66e047b 100644 --- a/src/command/graph/introspect.rs +++ b/src/command/graph/introspect.rs @@ -10,7 +10,7 @@ use rover_client::{ operations::graph::introspect::{self, GraphIntrospectInput}, }; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::parsers::parse_header; #[derive(Debug, Serialize, StructOpt)] @@ -31,7 +31,7 @@ pub struct Introspect { } impl Introspect { - pub fn run(&self, client: Client) -> Result { + pub fn run(&self, client: Client) -> Result { let client = GraphQLClient::new(&self.endpoint.to_string(), client)?; // add the flag headers to a hashmap to pass along to rover-client @@ -44,7 +44,7 @@ impl Introspect { let introspection_response = introspect::run(GraphIntrospectInput { headers }, &client)?; - Ok(RoverStdout::Introspection( + Ok(RoverOutput::Introspection( introspection_response.schema_sdl, )) } diff --git a/src/command/graph/mod.rs b/src/command/graph/mod.rs index 410690612..0603fb4e5 100644 --- a/src/command/graph/mod.rs +++ b/src/command/graph/mod.rs @@ -6,7 +6,7 @@ mod publish; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -39,7 +39,7 @@ impl Graph { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { match &self.command { Command::Check(command) => command.run(client_config, git_context), Command::Fetch(command) => command.run(client_config), diff --git a/src/command/graph/publish.rs b/src/command/graph/publish.rs index d005b993c..c6208e5df 100644 --- a/src/command/graph/publish.rs +++ b/src/command/graph/publish.rs @@ -2,10 +2,10 @@ use ansi_term::Colour::{Cyan, Yellow}; use serde::Serialize; use structopt::StructOpt; -use rover_client::operations::graph::publish::{self, GraphPublishInput, GraphPublishResponse}; +use rover_client::operations::graph::publish::{self, GraphPublishInput}; use rover_client::shared::{GitContext, GraphRef}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{parse_schema_source, SchemaSource}; @@ -36,7 +36,7 @@ impl Publish { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -58,39 +58,9 @@ impl Publish { &client, )?; - let hash = handle_response(&self.graph, publish_response); - Ok(RoverStdout::SchemaHash(hash)) - } -} - -/// handle all output logging from operation -fn handle_response(graph: &GraphRef, response: GraphPublishResponse) -> String { - eprintln!( - "{}#{} Published successfully {}", - graph, response.schema_hash, response.change_summary - ); - - response.schema_hash -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn handle_response_doesnt_err() { - let expected_hash = "123456".to_string(); - let graph = GraphRef { - name: "harambe".to_string(), - variant: "inside-job".to_string(), - }; - let actual_hash = handle_response( - &graph, - GraphPublishResponse { - schema_hash: expected_hash.clone(), - change_summary: "".to_string(), - }, - ); - assert_eq!(actual_hash, expected_hash); + Ok(RoverOutput::GraphPublishResponse { + graph_ref: self.graph.clone(), + publish_response, + }) } } diff --git a/src/command/info.rs b/src/command/info.rs index 55756b6e0..0f719690c 100644 --- a/src/command/info.rs +++ b/src/command/info.rs @@ -1,4 +1,4 @@ -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; use crate::PKG_VERSION; use serde::Serialize; @@ -9,7 +9,7 @@ use structopt::StructOpt; pub struct Info {} impl Info { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { let os = os_info::get(); // something like "/usr/bin/zsh" or "Unknown" @@ -28,6 +28,6 @@ impl Info { PKG_VERSION, location, os, shell ); - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/install/mod.rs b/src/command/install/mod.rs index ca2f34be3..293a4b941 100644 --- a/src/command/install/mod.rs +++ b/src/command/install/mod.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use binstall::Installer; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::PKG_NAME; use crate::{anyhow, Context, Result}; use crate::{command::docs::shortlinks, utils::env::RoverEnvKey}; @@ -20,7 +20,7 @@ pub struct Install { } impl Install { - pub fn run(&self, override_install_path: Option) -> Result { + pub fn run(&self, override_install_path: Option) -> Result { let binary_name = PKG_NAME.to_string(); if let Ok(executable_location) = env::current_exe() { let executable_location = Utf8PathBuf::try_from(executable_location)?; @@ -68,7 +68,7 @@ impl Install { } else { eprintln!("{} was not installed. To override the existing installation, you can pass the `--force` flag to the installer.", &binary_name); } - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } else { Err(anyhow!("Failed to get the current executable's path.").into()) } diff --git a/src/command/mod.rs b/src/command/mod.rs index 7d0b6c04b..7cf6e8bd3 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -16,7 +16,7 @@ pub use explain::Explain; pub use graph::Graph; pub use info::Info; pub use install::Install; -pub use output::RoverStdout; +pub use output::RoverOutput; pub use subgraph::Subgraph; pub use supergraph::Supergraph; pub use update::Update; diff --git a/src/command/output.rs b/src/command/output.rs index 141495f8e..146139540 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -1,43 +1,65 @@ -use std::fmt::Debug; -use std::{collections::HashMap, fmt::Display}; +use std::collections::BTreeMap; +use std::fmt::{self, Debug, Display}; +use crate::error::RoverError; use crate::utils::table::{self, cell, row}; -use ansi_term::{Colour::Yellow, Style}; +use ansi_term::{ + Colour::{Cyan, Red, Yellow}, + Style, +}; use atty::Stream; use crossterm::style::Attribute::Underlined; +use rover_client::operations::graph::publish::GraphPublishResponse; +use rover_client::operations::subgraph::delete::SubgraphDeleteResponse; use rover_client::operations::subgraph::list::SubgraphListResponse; -use rover_client::shared::{CheckResponse, FetchResponse, SdlType}; +use rover_client::operations::subgraph::publish::SubgraphPublishResponse; +use rover_client::shared::{CheckResponse, FetchResponse, GraphRef, SdlType}; +use rover_client::RoverClientError; +use serde::Serialize; +use serde_json::{json, Value}; use termimad::MadSkin; -/// RoverStdout defines all of the different types of data that are printed -/// to `stdout`. Every one of Rover's commands should return `anyhow::Result` +/// RoverOutput defines all of the different types of data that are printed +/// to `stdout`. Every one of Rover's commands should return `anyhow::Result` /// If the command needs to output some type of data, it should be structured -/// in this enum, and its print logic should be handled in `RoverStdout::print` +/// in this enum, and its print logic should be handled in `RoverOutput::print` /// /// Not all commands will output machine readable information, and those should -/// return `Ok(RoverStdout::None)`. If a new command is added and it needs to +/// return `Ok(RoverOutput::EmptySuccess)`. If a new command is added and it needs to /// return something that is not described well in this enum, it should be added. #[derive(Clone, PartialEq, Debug)] -pub enum RoverStdout { - DocsList(HashMap<&'static str, &'static str>), +pub enum RoverOutput { + DocsList(BTreeMap<&'static str, &'static str>), FetchResponse(FetchResponse), CoreSchema(String), - SchemaHash(String), SubgraphList(SubgraphListResponse), CheckResponse(CheckResponse), - VariantList(Vec), + GraphPublishResponse { + graph_ref: GraphRef, + publish_response: GraphPublishResponse, + }, + SubgraphPublishResponse { + graph_ref: GraphRef, + subgraph: String, + publish_response: SubgraphPublishResponse, + }, + SubgraphDeleteResponse { + graph_ref: GraphRef, + subgraph: String, + dry_run: bool, + delete_response: SubgraphDeleteResponse, + }, Profiles(Vec), Introspection(String), - Markdown(String), - PlainText(String), - None, + ErrorExplanation(String), + EmptySuccess, } -impl RoverStdout { +impl RoverOutput { pub fn print(&self) { match self { - RoverStdout::DocsList(shortlinks) => { + RoverOutput::DocsList(shortlinks) => { eprintln!( "You can open any of these documentation pages by running {}.\n", Yellow.normal().paint("`rover docs open `") @@ -51,22 +73,108 @@ impl RoverStdout { } println!("{}", table); } - RoverStdout::FetchResponse(fetch_response) => { + RoverOutput::FetchResponse(fetch_response) => { match fetch_response.sdl.r#type { SdlType::Graph | SdlType::Subgraph => print_descriptor("SDL"), SdlType::Supergraph => print_descriptor("Supergraph SDL"), } print_content(&fetch_response.sdl.contents); } - RoverStdout::CoreSchema(csdl) => { + RoverOutput::GraphPublishResponse { + graph_ref, + publish_response, + } => { + eprintln!( + "{}#{} Published successfully {}", + graph_ref, publish_response.api_schema_hash, publish_response.change_summary + ); + print_one_line_descriptor("Schema Hash"); + print_content(&publish_response.api_schema_hash); + } + RoverOutput::SubgraphPublishResponse { + graph_ref, + subgraph, + publish_response, + } => { + if publish_response.subgraph_was_created { + eprintln!( + "A new subgraph called '{}' for the '{}' graph was created", + subgraph, graph_ref + ); + } else { + eprintln!( + "The '{}' subgraph for the '{}' graph was updated", + subgraph, graph_ref + ); + } + + if publish_response.supergraph_was_updated { + eprintln!("The gateway for the '{}' graph was updated with a new schema, composed from the updated '{}' subgraph", graph_ref, subgraph); + } else { + eprintln!( + "The gateway for the '{}' graph was NOT updated with a new schema", + graph_ref + ); + } + + if !publish_response.build_errors.is_empty() { + let warn_prefix = Red.normal().paint("WARN:"); + eprintln!("{} The following build errors occurred:", warn_prefix,); + eprintln!("{}", &publish_response.build_errors); + } + } + RoverOutput::SubgraphDeleteResponse { + graph_ref, + subgraph, + dry_run, + delete_response, + } => { + let warn_prefix = Red.normal().paint("WARN:"); + if *dry_run { + if !delete_response.build_errors.is_empty() { + eprintln!( + "{} Deleting the {} subgraph from {} would result in the following build errors:", + warn_prefix, + Cyan.normal().paint(subgraph), + Cyan.normal().paint(graph_ref.to_string()), + ); + + eprintln!("{}", &delete_response.build_errors); + eprintln!("{} This is only a prediction. If the graph changes before confirming, these errors could change.", warn_prefix); + } else { + eprintln!("{} At the time of checking, there would be no build errors resulting from the deletion of this subgraph.", warn_prefix); + eprintln!("{} This is only a prediction. If the graph changes before confirming, there could be build errors.", warn_prefix) + } + } else { + if delete_response.supergraph_was_updated { + eprintln!( + "The {} subgraph was removed from {}. Remaining subgraphs were composed.", + Cyan.normal().paint(subgraph), + Cyan.normal().paint(graph_ref.to_string()), + ) + } else { + eprintln!( + "{} The gateway for {} was not updated. See errors below.", + warn_prefix, + Cyan.normal().paint(graph_ref.to_string()) + ) + } + + if !delete_response.build_errors.is_empty() { + eprintln!( + "{} There were build errors as a result of deleting the subgraph:", + warn_prefix, + ); + + eprintln!("{}", &delete_response.build_errors); + } + } + } + RoverOutput::CoreSchema(csdl) => { print_descriptor("CoreSchema"); print_content(&csdl); } - RoverStdout::SchemaHash(hash) => { - print_one_line_descriptor("Schema Hash"); - print_content(&hash); - } - RoverStdout::SubgraphList(details) => { + RoverOutput::SubgraphList(details) => { let mut table = table::get_table(); // bc => sets top row to be bold and center @@ -83,7 +191,7 @@ impl RoverStdout { } else { url }; - let formatted_updated_at: String = if let Some(dt) = subgraph.updated_at { + let formatted_updated_at: String = if let Some(dt) = subgraph.updated_at.local { dt.format("%Y-%m-%d %H:%M:%S %Z").to_string() } else { "N/A".to_string() @@ -98,16 +206,11 @@ impl RoverStdout { details.root_url, details.graph_ref.name ); } - RoverStdout::CheckResponse(check_response) => { - print_check_response(check_response); - } - RoverStdout::VariantList(variants) => { - print_descriptor("Variants"); - for variant in variants { - println!("{}", variant); - } + RoverOutput::CheckResponse(check_response) => { + print_descriptor("Check Result"); + print_content(check_response.get_table()); } - RoverStdout::Profiles(profiles) => { + RoverOutput::Profiles(profiles) => { if profiles.is_empty() { eprintln!("No profiles found."); } else { @@ -118,23 +221,101 @@ impl RoverStdout { println!("{}", profile); } } - RoverStdout::Introspection(introspection_response) => { + RoverOutput::Introspection(introspection_response) => { print_descriptor("Introspection Response"); print_content(&introspection_response); } - RoverStdout::Markdown(markdown_string) => { + RoverOutput::ErrorExplanation(explanation) => { // underline bolded md let mut skin = MadSkin::default(); skin.bold.add_attr(Underlined); - println!("{}", skin.inline(&markdown_string)); + println!("{}", skin.inline(&explanation)); + } + RoverOutput::EmptySuccess => (), + } + } + + pub(crate) fn get_internal_data_json(&self) -> Value { + match self { + RoverOutput::DocsList(shortlinks) => { + let mut shortlink_vec = Vec::with_capacity(shortlinks.len()); + for (shortlink_slug, shortlink_description) in shortlinks { + shortlink_vec.push( + json!({"slug": shortlink_slug, "description": shortlink_description }), + ); + } + json!({ "shortlinks": shortlink_vec }) } - RoverStdout::PlainText(text) => { - println!("{}", text); + RoverOutput::FetchResponse(fetch_response) => json!(fetch_response), + RoverOutput::CoreSchema(csdl) => json!({ "core_schema": csdl }), + RoverOutput::GraphPublishResponse { + graph_ref: _, + publish_response, + } => json!(publish_response), + RoverOutput::SubgraphPublishResponse { + graph_ref: _, + subgraph: _, + publish_response, + } => json!(publish_response), + RoverOutput::SubgraphDeleteResponse { + graph_ref: _, + subgraph: _, + dry_run: _, + delete_response, + } => { + json!(delete_response) } - RoverStdout::None => (), + RoverOutput::SubgraphList(list_response) => json!(list_response), + RoverOutput::CheckResponse(check_response) => check_response.get_json(), + RoverOutput::Profiles(profiles) => json!({ "profiles": profiles }), + RoverOutput::Introspection(introspection_response) => { + json!({ "introspection_response": introspection_response }) + } + RoverOutput::ErrorExplanation(explanation_markdown) => { + json!({ "explanation_markdown": explanation_markdown }) + } + RoverOutput::EmptySuccess => json!(null), } } + + pub(crate) fn get_internal_error_json(&self) -> Value { + let rover_error = match self { + RoverOutput::SubgraphPublishResponse { + graph_ref, + subgraph, + publish_response, + } => { + if !publish_response.build_errors.is_empty() { + Some(RoverError::from(RoverClientError::SubgraphBuildErrors { + subgraph: subgraph.clone(), + graph_ref: graph_ref.clone(), + source: publish_response.build_errors.clone(), + })) + } else { + None + } + } + RoverOutput::SubgraphDeleteResponse { + graph_ref, + subgraph, + dry_run: _, + delete_response, + } => { + if !delete_response.build_errors.is_empty() { + Some(RoverError::from(RoverClientError::SubgraphBuildErrors { + subgraph: subgraph.clone(), + graph_ref: graph_ref.clone(), + source: delete_response.build_errors.clone(), + })) + } else { + None + } + } + _ => None, + }; + json!(rover_error) + } } fn print_descriptor(descriptor: impl Display) { @@ -159,32 +340,755 @@ fn print_content(content: impl Display) { } } -pub(crate) fn print_check_response(check_response: &CheckResponse) { - let num_changes = check_response.changes.len(); +#[derive(Debug, Clone, Serialize)] +pub(crate) struct JsonOutput { + json_version: JsonVersion, + data: JsonData, + error: Value, +} + +impl JsonOutput { + pub(crate) fn success(data: Value, error: Value) -> JsonOutput { + JsonOutput { + json_version: JsonVersion::OneBeta, + data: JsonData::success(data), + error, + } + } + + pub(crate) fn failure(data: Value, error: Value) -> JsonOutput { + JsonOutput { + json_version: JsonVersion::OneBeta, + data: JsonData::failure(data), + error, + } + } +} + +impl fmt::Display for JsonOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", json!(self)) + } +} + +impl From for JsonOutput { + fn from(error: RoverError) -> Self { + let data = error.get_internal_data_json(); + let error = error.get_internal_error_json(); + JsonOutput::failure(data, error) + } +} + +impl From for JsonOutput { + fn from(output: RoverOutput) -> Self { + let data = output.get_internal_data_json(); + let error = output.get_internal_error_json(); + JsonOutput::success(data, error) + } +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct JsonData { + #[serde(flatten)] + inner: Value, + success: bool, +} + +impl JsonData { + pub(crate) fn success(inner: Value) -> JsonData { + JsonData { + inner, + success: true, + } + } + + pub(crate) fn failure(inner: Value) -> JsonData { + JsonData { + inner, + success: false, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) enum JsonVersion { + #[serde(rename = "1.beta")] + OneBeta, +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; - let msg = match num_changes { - 0 => "There were no changes detected in the composed schema.".to_string(), - _ => format!( - "Compared {} schema changes against {} operations", - num_changes, check_response.number_of_checked_operations - ), + use assert_json_diff::assert_json_eq; + use chrono::{DateTime, Local, Utc}; + use rover_client::{ + operations::{ + graph::publish::{ChangeSummary, FieldChanges, TypeChanges}, + subgraph::{ + delete::SubgraphDeleteResponse, + list::{SubgraphInfo, SubgraphUpdatedAt}, + }, + }, + shared::{BuildError, BuildErrors, ChangeSeverity, SchemaChange, Sdl}, }; - eprintln!("{}", &msg); + use crate::anyhow; + + use super::*; + + #[test] + fn docs_list_json() { + let mut mock_shortlinks = BTreeMap::new(); + mock_shortlinks.insert("slug_one", "description_one"); + mock_shortlinks.insert("slug_two", "description_two"); + let actual_json: JsonOutput = RoverOutput::DocsList(mock_shortlinks).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "shortlinks": [ + { + "slug": "slug_one", + "description": "description_one" + }, + { + "slug": "slug_two", + "description": "description_two" + } + ], + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn fetch_response_json() { + let mock_fetch_response = FetchResponse { + sdl: Sdl { + contents: "sdl contents".to_string(), + r#type: SdlType::Subgraph, + }, + }; + let actual_json: JsonOutput = RoverOutput::FetchResponse(mock_fetch_response).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "sdl": { + "contents": "sdl contents", + }, + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn core_schema_json() { + let mock_core_schema = "core schema contents".to_string(); + let actual_json: JsonOutput = RoverOutput::CoreSchema(mock_core_schema).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "core_schema": "core schema contents", + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn subgraph_list_json() { + let now_utc: DateTime = Utc::now(); + let now_local: DateTime = now_utc.into(); + let mock_subgraph_list_response = SubgraphListResponse { + subgraphs: vec![ + SubgraphInfo { + name: "subgraph one".to_string(), + url: Some("http://localhost:4001".to_string()), + updated_at: SubgraphUpdatedAt { + local: Some(now_local), + utc: Some(now_utc), + }, + }, + SubgraphInfo { + name: "subgraph two".to_string(), + url: None, + updated_at: SubgraphUpdatedAt { + local: None, + utc: None, + }, + }, + ], + root_url: "https://studio.apollographql.com/".to_string(), + graph_ref: GraphRef { + name: "graph".to_string(), + variant: "current".to_string(), + }, + }; + let actual_json: JsonOutput = RoverOutput::SubgraphList(mock_subgraph_list_response).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "subgraphs": [ + { + "name": "subgraph one", + "url": "http://localhost:4001", + "updated_at": { + "local": now_local, + "utc": now_utc + } + }, + { + "name": "subgraph two", + "url": null, + "updated_at": { + "local": null, + "utc": null + } + } + ], + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn subgraph_delete_success_json() { + let mock_subgraph_delete = SubgraphDeleteResponse { + supergraph_was_updated: true, + build_errors: BuildErrors::new(), + }; + let actual_json: JsonOutput = RoverOutput::SubgraphDeleteResponse { + delete_response: mock_subgraph_delete, + subgraph: "subgraph".to_string(), + dry_run: false, + graph_ref: GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }, + } + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "supergraph_was_updated": true, + "success": true, + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn subgraph_delete_build_errors_json() { + let mock_subgraph_delete = SubgraphDeleteResponse { + supergraph_was_updated: false, + build_errors: vec![ + BuildError::composition_error( + "[Accounts] -> Things went really wrong".to_string(), + Some("AN_ERROR_CODE".to_string()), + ), + BuildError::composition_error( + "[Films] -> Something else also went wrong".to_string(), + None, + ), + ] + .into(), + }; + let actual_json: JsonOutput = RoverOutput::SubgraphDeleteResponse { + delete_response: mock_subgraph_delete, + subgraph: "subgraph".to_string(), + dry_run: true, + graph_ref: GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }, + } + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "supergraph_was_updated": false, + "success": true, + }, + "error": { + "message": "Encountered 2 build errors while trying to build subgraph \"subgraph\" into supergraph \"name@current\".", + "code": "E029", + "details": { + "build_errors": [ + { + "message": "[Accounts] -> Things went really wrong", + "code": "AN_ERROR_CODE", + "type": "composition" + }, + { + "message": "[Films] -> Something else also went wrong", + "code": null, + "type": "composition" + } + ], + } + } + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn supergraph_fetch_no_successful_publishes_json() { + let graph_ref = GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }; + let source = BuildErrors::from(vec![ + BuildError::composition_error( + "[Accounts] -> Things went really wrong".to_string(), + Some("AN_ERROR_CODE".to_string()), + ), + BuildError::composition_error( + "[Films] -> Something else also went wrong".to_string(), + None, + ), + ]); + let actual_json: JsonOutput = + RoverError::new(RoverClientError::NoSupergraphBuilds { graph_ref, source }).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "success": false + }, + "error": { + "message": "No supergraph SDL exists for \"name@current\" because its subgraphs failed to build.", + "details": { + "build_errors": [ + { + "message": "[Accounts] -> Things went really wrong", + "code": "AN_ERROR_CODE", + "type": "composition", + }, + { + "message": "[Films] -> Something else also went wrong", + "code": null, + "type": "composition" + } + ] + }, + "code": "E027" + } + }); + assert_json_eq!(actual_json, expected_json); + } + + #[test] + fn check_success_response_json() { + let graph_ref = GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }; + let mock_check_response = CheckResponse::try_new( + Some("https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current".to_string()), + 10, + vec![ + SchemaChange { + code: "SOMETHING_HAPPENED".to_string(), + description: "beeg yoshi".to_string(), + severity: ChangeSeverity::PASS, + }, + SchemaChange { + code: "WOW".to_string(), + description: "that was so cool".to_string(), + severity: ChangeSeverity::PASS, + } + ], + ChangeSeverity::PASS, + graph_ref, + ); + if let Ok(mock_check_response) = mock_check_response { + let actual_json: JsonOutput = RoverOutput::CheckResponse(mock_check_response).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "target_url": "https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current", + "operation_check_count": 10, + "changes": [ + { + "code": "SOMETHING_HAPPENED", + "description": "beeg yoshi", + "severity": "PASS" + }, + { + "code": "WOW", + "description": "that was so cool", + "severity": "PASS" + }, + ], + "failure_count": 0, + "success": true, + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } else { + panic!("The shape of this response should return a CheckResponse") + } + } + + #[test] + fn check_failure_response_json() { + let graph_ref = GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }; + let check_response = CheckResponse::try_new( + Some("https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current".to_string()), + 10, + vec![ + SchemaChange { + code: "SOMETHING_HAPPENED".to_string(), + description: "beeg yoshi".to_string(), + severity: ChangeSeverity::FAIL, + }, + SchemaChange { + code: "WOW".to_string(), + description: "that was so cool".to_string(), + severity: ChangeSeverity::FAIL, + } + ], + ChangeSeverity::FAIL, graph_ref); + + if let Err(operation_check_failure) = check_response { + let actual_json: JsonOutput = RoverError::new(operation_check_failure).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "target_url": "https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current", + "operation_check_count": 10, + "changes": [ + { + "code": "SOMETHING_HAPPENED", + "description": "beeg yoshi", + "severity": "FAIL" + }, + { + "code": "WOW", + "description": "that was so cool", + "severity": "FAIL" + }, + ], + "failure_count": 2, + "success": false, + }, + "error": { + "message": "This operation check has encountered 2 schema changes that would break operations from existing client traffic.", + "code": "E030", + } + }); + assert_json_eq!(expected_json, actual_json); + } else { + panic!("The shape of this response should return a RoverClientError") + } + } + + #[test] + fn graph_publish_response_json() { + let mock_publish_response = GraphPublishResponse { + api_schema_hash: "123456".to_string(), + change_summary: ChangeSummary { + field_changes: FieldChanges { + additions: 2, + removals: 1, + edits: 0, + }, + type_changes: TypeChanges { + additions: 4, + removals: 0, + edits: 7, + }, + }, + }; + let actual_json: JsonOutput = RoverOutput::GraphPublishResponse { + graph_ref: GraphRef { + name: "graph".to_string(), + variant: "variant".to_string(), + }, + publish_response: mock_publish_response, + } + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "api_schema_hash": "123456", + "field_changes": { + "additions": 2, + "removals": 1, + "edits": 0 + }, + "type_changes": { + "additions": 4, + "removals": 0, + "edits": 7 + }, + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn subgraph_publish_success_response_json() { + let mock_publish_response = SubgraphPublishResponse { + api_schema_hash: Some("123456".to_string()), + build_errors: BuildErrors::new(), + supergraph_was_updated: true, + subgraph_was_created: true, + }; + let actual_json: JsonOutput = RoverOutput::SubgraphPublishResponse { + graph_ref: GraphRef { + name: "graph".to_string(), + variant: "variant".to_string(), + }, + subgraph: "subgraph".to_string(), + publish_response: mock_publish_response, + } + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "api_schema_hash": "123456", + "supergraph_was_updated": true, + "subgraph_was_created": true, + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } - if !check_response.changes.is_empty() { - let mut table = table::get_table(); + #[test] + fn subgraph_publish_failure_response_json() { + let mock_publish_response = SubgraphPublishResponse { + api_schema_hash: None, - // bc => sets top row to be bold and center - table.add_row(row![bc => "Change", "Code", "Description"]); - for check in &check_response.changes { - table.add_row(row![check.severity, check.code, check.description]); + build_errors: vec![ + BuildError::composition_error( + "[Accounts] -> Things went really wrong".to_string(), + Some("AN_ERROR_CODE".to_string()), + ), + BuildError::composition_error( + "[Films] -> Something else also went wrong".to_string(), + None, + ), + ] + .into(), + supergraph_was_updated: false, + subgraph_was_created: false, + }; + let actual_json: JsonOutput = RoverOutput::SubgraphPublishResponse { + graph_ref: GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }, + subgraph: "subgraph".to_string(), + publish_response: mock_publish_response, } + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "api_schema_hash": null, + "subgraph_was_created": false, + "supergraph_was_updated": false, + "success": true + }, + "error": { + "message": "Encountered 2 build errors while trying to build subgraph \"subgraph\" into supergraph \"name@current\".", + "code": "E029", + "details": { + "build_errors": [ + { + "message": "[Accounts] -> Things went really wrong", + "code": "AN_ERROR_CODE", + "type": "composition", + }, + { + "message": "[Films] -> Something else also went wrong", + "code": null, + "type": "composition" + } + ] + } + } + }); + assert_json_eq!(expected_json, actual_json); + } - print_content(&table); + #[test] + fn profiles_json() { + let mock_profiles = vec!["default".to_string(), "staging".to_string()]; + let actual_json: JsonOutput = RoverOutput::Profiles(mock_profiles).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "profiles": [ + "default", + "staging" + ], + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); } - if let Some(url) = &check_response.target_url { - eprintln!("View full details at {}", url); + #[test] + fn introspection_json() { + let actual_json: JsonOutput = RoverOutput::Introspection( + "i cant believe its not a real introspection response".to_string(), + ) + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "introspection_response": "i cant believe its not a real introspection response", + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn error_explanation_json() { + let actual_json: JsonOutput = RoverOutput::ErrorExplanation( + "this error occurs when stuff is real complicated... I wouldn't worry about it" + .to_string(), + ) + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "explanation_markdown": "this error occurs when stuff is real complicated... I wouldn't worry about it", + "success": true + }, + "error": null + } + + ); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn empty_success_json() { + let actual_json: JsonOutput = RoverOutput::EmptySuccess.into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn base_error_message_json() { + let actual_json: JsonOutput = RoverError::new(anyhow!("Some random error")).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "success": false + }, + "error": { + "message": "Some random error", + "code": null + } + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn coded_error_message_json() { + let actual_json: JsonOutput = RoverError::new(RoverClientError::NoSubgraphInGraph { + invalid_subgraph: "invalid_subgraph".to_string(), + valid_subgraphs: Vec::new(), + }) + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "success": false + }, + "error": { + "message": "Could not find subgraph \"invalid_subgraph\".", + "code": "E009" + } + }); + assert_json_eq!(expected_json, actual_json) + } + + #[test] + fn composition_error_message_json() { + let source = BuildErrors::from(vec![ + BuildError::composition_error( + "[Accounts] -> Things went really wrong".to_string(), + Some("AN_ERROR_CODE".to_string()), + ), + BuildError::composition_error( + "[Films] -> Something else also went wrong".to_string(), + None, + ), + ]); + let actual_json: JsonOutput = + RoverError::from(RoverClientError::BuildErrors { source }).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "success": false + }, + "error": { + "details": { + "build_errors": [ + { + "message": "[Accounts] -> Things went really wrong", + "code": "AN_ERROR_CODE", + "type": "composition" + }, + { + "message": "[Films] -> Something else also went wrong", + "code": null, + "type": "composition" + } + ], + }, + "message": "Encountered 2 build errors while trying to build a supergraph.", + "code": "E029" + } + }); + assert_json_eq!(expected_json, actual_json) } } diff --git a/src/command/subgraph/check.rs b/src/command/subgraph/check.rs index 67201bee4..696ae2b2f 100644 --- a/src/command/subgraph/check.rs +++ b/src/command/subgraph/check.rs @@ -4,7 +4,7 @@ use structopt::StructOpt; use rover_client::operations::subgraph::check::{self, SubgraphCheckInput}; use rover_client::shared::{CheckConfig, GitContext, GraphRef, ValidationPeriod}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{ @@ -58,7 +58,7 @@ impl Check { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let proposed_schema = load_schema_from_flag(&self.schema, std::io::stdin())?; @@ -83,6 +83,6 @@ impl Check { &client, )?; - Ok(RoverStdout::CheckResponse(res)) + Ok(RoverOutput::CheckResponse(res)) } } diff --git a/src/command/subgraph/delete.rs b/src/command/subgraph/delete.rs index de53867cb..4e8c3ac9e 100644 --- a/src/command/subgraph/delete.rs +++ b/src/command/subgraph/delete.rs @@ -1,14 +1,12 @@ -use ansi_term::Colour::{Cyan, Red, Yellow}; +use ansi_term::Colour::{Cyan, Yellow}; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; -use rover_client::operations::subgraph::delete::{ - self, SubgraphDeleteInput, SubgraphDeleteResponse, -}; +use rover_client::operations::subgraph::delete::{self, SubgraphDeleteInput}; use rover_client::shared::GraphRef; #[derive(Debug, Serialize, StructOpt)] @@ -30,75 +28,68 @@ pub struct Delete { subgraph: String, /// Skips the step where the command asks for user confirmation before - /// deleting the subgraph. Also skips preview of composition errors that + /// deleting the subgraph. Also skips preview of build errors that /// might occur #[structopt(long)] confirm: bool, } impl Delete { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; - let graph_ref = self.graph.to_string(); eprintln!( - "Checking for composition errors resulting from deleting subgraph {} from {} using credentials from the {} profile.", + "Checking for build errors resulting from deleting subgraph {} from {} using credentials from the {} profile.", Cyan.normal().paint(&self.subgraph), - Cyan.normal().paint(&graph_ref), + Cyan.normal().paint(self.graph.to_string()), Yellow.normal().paint(&self.profile_name) ); // this is probably the normal path -- preview a subgraph delete // and make the user confirm it manually. if !self.confirm { - // run delete with dryRun, so we can preview composition errors + let dry_run = true; + // run delete with dryRun, so we can preview build errors let delete_dry_run_response = delete::run( SubgraphDeleteInput { graph_ref: self.graph.clone(), subgraph: self.subgraph.clone(), - dry_run: true, + dry_run, }, &client, )?; - handle_dry_run_response(delete_dry_run_response, &self.subgraph, &graph_ref); + RoverOutput::SubgraphDeleteResponse { + graph_ref: self.graph.clone(), + subgraph: self.subgraph.clone(), + dry_run, + delete_response: delete_dry_run_response, + } + .print(); // I chose not to error here, since this is a perfectly valid path if !confirm_delete()? { eprintln!("Delete cancelled by user"); - return Ok(RoverStdout::None); + return Ok(RoverOutput::EmptySuccess); } } + let dry_run = false; + let delete_response = delete::run( SubgraphDeleteInput { graph_ref: self.graph.clone(), subgraph: self.subgraph.clone(), - dry_run: false, + dry_run, }, &client, )?; - handle_response(delete_response, &self.subgraph, &graph_ref); - Ok(RoverStdout::None) - } -} - -fn handle_dry_run_response(response: SubgraphDeleteResponse, subgraph: &str, graph_ref: &str) { - let warn_prefix = Red.normal().paint("WARN:"); - if let Some(errors) = response.composition_errors { - eprintln!( - "{} Deleting the {} subgraph from {} would result in the following composition errors:", - warn_prefix, - Cyan.normal().paint(subgraph), - Cyan.normal().paint(graph_ref), - ); - for error in errors { - eprintln!("{}", &error); - } - eprintln!("{} This is only a prediction. If the graph changes before confirming, these errors could change.", warn_prefix); - } else { - eprintln!("{} At the time of checking, there would be no composition errors resulting from the deletion of this subgraph.", warn_prefix); - eprintln!("{} This is only a prediction. If the graph changes before confirming, there could be composition errors.", warn_prefix) + Ok(RoverOutput::SubgraphDeleteResponse { + graph_ref: self.graph.clone(), + subgraph: self.subgraph.clone(), + dry_run, + delete_response, + }) } } @@ -112,69 +103,3 @@ fn confirm_delete() -> Result { Ok(false) } } - -fn handle_response(response: SubgraphDeleteResponse, subgraph: &str, graph_ref: &str) { - let warn_prefix = Red.normal().paint("WARN:"); - if response.updated_gateway { - eprintln!( - "The {} subgraph was removed from {}. Remaining subgraphs were composed.", - Cyan.normal().paint(subgraph), - Cyan.normal().paint(graph_ref), - ) - } else { - eprintln!( - "{} The gateway for {} was not updated. See errors below.", - warn_prefix, - Cyan.normal().paint(graph_ref) - ) - } - - if let Some(errors) = response.composition_errors { - eprintln!( - "{} There were composition errors as a result of deleting the subgraph:", - warn_prefix, - ); - - for error in errors { - eprintln!("{}", &error); - } - } -} - -#[cfg(test)] -mod tests { - use super::{handle_response, SubgraphDeleteResponse}; - use rover_client::shared::CompositionError; - - #[test] - fn handle_response_doesnt_error_with_all_successes() { - let response = SubgraphDeleteResponse { - composition_errors: None, - updated_gateway: true, - }; - - handle_response(response, "accounts", "my-graph@current"); - } - - #[test] - fn handle_response_doesnt_error_with_all_failures() { - let response = SubgraphDeleteResponse { - composition_errors: Some(vec![ - CompositionError { - message: "a bad thing happened".to_string(), - code: None, - }, - CompositionError { - message: "another bad thing".to_string(), - code: None, - }, - ]), - updated_gateway: false, - }; - - handle_response(response, "accounts", "my-graph@prod"); - } - - // TODO: test the actual output of the logs whenever we do design work - // for the commands :) -} diff --git a/src/command/subgraph/fetch.rs b/src/command/subgraph/fetch.rs index ba7f35bc7..92643e71b 100644 --- a/src/command/subgraph/fetch.rs +++ b/src/command/subgraph/fetch.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use rover_client::operations::subgraph::fetch::{self, SubgraphFetchInput}; use rover_client::shared::GraphRef; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -29,7 +29,7 @@ pub struct Fetch { } impl Fetch { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -47,6 +47,6 @@ impl Fetch { &client, )?; - Ok(RoverStdout::FetchResponse(fetch_response)) + Ok(RoverOutput::FetchResponse(fetch_response)) } } diff --git a/src/command/subgraph/introspect.rs b/src/command/subgraph/introspect.rs index bdce9cdea..5b527495d 100644 --- a/src/command/subgraph/introspect.rs +++ b/src/command/subgraph/introspect.rs @@ -9,7 +9,7 @@ use rover_client::{ operations::subgraph::introspect::{self, SubgraphIntrospectInput}, }; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::parsers::parse_header; use crate::Result; @@ -36,7 +36,7 @@ pub struct Introspect { } impl Introspect { - pub fn run(&self, client: Client) -> Result { + pub fn run(&self, client: Client) -> Result { let client = GraphQLClient::new(&self.endpoint.to_string(), client)?; // add the flag headers to a hashmap to pass along to rover-client @@ -49,6 +49,6 @@ impl Introspect { let introspection_response = introspect::run(SubgraphIntrospectInput { headers }, &client)?; - Ok(RoverStdout::Introspection(introspection_response.result)) + Ok(RoverOutput::Introspection(introspection_response.result)) } } diff --git a/src/command/subgraph/list.rs b/src/command/subgraph/list.rs index 4bcbc312c..c1794801e 100644 --- a/src/command/subgraph/list.rs +++ b/src/command/subgraph/list.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use rover_client::operations::subgraph::list::{self, SubgraphListInput}; use rover_client::shared::GraphRef; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -24,7 +24,7 @@ pub struct List { } impl List { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; eprintln!( @@ -40,6 +40,6 @@ impl List { &client, )?; - Ok(RoverStdout::SubgraphList(list_details)) + Ok(RoverOutput::SubgraphList(list_details)) } } diff --git a/src/command/subgraph/mod.rs b/src/command/subgraph/mod.rs index ee1ff3704..08ba2adb0 100644 --- a/src/command/subgraph/mod.rs +++ b/src/command/subgraph/mod.rs @@ -8,7 +8,7 @@ mod publish; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -22,7 +22,7 @@ pub struct Subgraph { #[derive(Debug, Serialize, StructOpt)] pub enum Command { - /// Check for composition errors and breaking changes caused by an updated subgraph schema + /// Check for build errors and breaking changes caused by an updated subgraph schema /// against the federated graph in the Apollo graph registry Check(check::Check), @@ -47,7 +47,7 @@ impl Subgraph { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { match &self.command { Command::Publish(command) => command.run(client_config, git_context), Command::Introspect(command) => command.run(client_config.get_reqwest_client()), diff --git a/src/command/subgraph/publish.rs b/src/command/subgraph/publish.rs index 5ee695359..4d2f4490e 100644 --- a/src/command/subgraph/publish.rs +++ b/src/command/subgraph/publish.rs @@ -1,8 +1,8 @@ -use ansi_term::Colour::{Cyan, Red, Yellow}; +use ansi_term::Colour::{Cyan, Yellow}; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::{ client::StudioClientConfig, loaders::load_schema_from_flag, @@ -10,9 +10,7 @@ use crate::utils::{ }; use crate::Result; -use rover_client::operations::subgraph::publish::{ - self, SubgraphPublishInput, SubgraphPublishResponse, -}; +use rover_client::operations::subgraph::publish::{self, SubgraphPublishInput}; use rover_client::shared::{GitContext, GraphRef}; #[derive(Debug, Serialize, StructOpt)] @@ -56,12 +54,11 @@ impl Publish { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; - let graph_ref = format!("{}:{}", &self.graph.name, &self.graph.variant); eprintln!( "Publishing SDL to {} (subgraph: {}) using credentials from the {} profile.", - Cyan.normal().paint(&graph_ref), + Cyan.normal().paint(&self.graph.to_string()), Cyan.normal().paint(&self.subgraph), Yellow.normal().paint(&self.profile_name) ); @@ -82,82 +79,10 @@ impl Publish { &client, )?; - handle_publish_response(publish_response, &self.subgraph, &self.graph.name); - Ok(RoverStdout::None) - } -} - -fn handle_publish_response(response: SubgraphPublishResponse, subgraph: &str, graph: &str) { - if response.subgraph_was_created { - eprintln!( - "A new subgraph called '{}' for the '{}' graph was created", - subgraph, graph - ); - } else { - eprintln!( - "The '{}' subgraph for the '{}' graph was updated", - subgraph, graph - ); - } - - if response.did_update_gateway { - eprintln!("The gateway for the '{}' graph was updated with a new schema, composed from the updated '{}' subgraph", graph, subgraph); - } else { - eprintln!( - "The gateway for the '{}' graph was NOT updated with a new schema", - graph - ); + Ok(RoverOutput::SubgraphPublishResponse { + graph_ref: self.graph.clone(), + subgraph: self.subgraph.clone(), + publish_response, + }) } - - if let Some(errors) = response.composition_errors { - let warn_prefix = Red.normal().paint("WARN:"); - eprintln!("{} The following composition errors occurred:", warn_prefix,); - for error in errors { - eprintln!("{}", &error); - } - } -} - -#[cfg(test)] -mod tests { - use super::{handle_publish_response, SubgraphPublishResponse}; - use rover_client::shared::CompositionError; - - // this test is a bit weird, since we can't test the output. We just verify it - // doesn't error - #[test] - fn handle_response_doesnt_error_with_all_successes() { - let response = SubgraphPublishResponse { - schema_hash: Some("123456".to_string()), - did_update_gateway: true, - subgraph_was_created: true, - composition_errors: None, - }; - - handle_publish_response(response, "accounts", "my-graph"); - } - - #[test] - fn handle_response_doesnt_error_with_all_failures() { - let response = SubgraphPublishResponse { - schema_hash: None, - did_update_gateway: false, - subgraph_was_created: false, - composition_errors: Some(vec![ - CompositionError { - message: "a bad thing happened".to_string(), - code: None, - }, - CompositionError { - message: "another bad thing".to_string(), - code: None, - }, - ]), - }; - - handle_publish_response(response, "accounts", "my-graph"); - } - - // TODO: test the actual output of the logs whenever we do design work - // for the commands :) } diff --git a/src/command/supergraph/compose/do_compose.rs b/src/command/supergraph/compose/do_compose.rs index b51d949c8..8fd80b813 100644 --- a/src/command/supergraph/compose/do_compose.rs +++ b/src/command/supergraph/compose/do_compose.rs @@ -1,22 +1,19 @@ use crate::command::supergraph::config::{self, SchemaSource, SupergraphConfig}; use crate::utils::client::StudioClientConfig; -use crate::{anyhow, command::RoverStdout, error::RoverError, Result, Suggestion}; +use crate::{anyhow, command::RoverOutput, error::RoverError, Result, Suggestion}; -use ansi_term::Colour::Red; -use camino::Utf8PathBuf; +use rover_client::blocking::GraphQLClient; +use rover_client::operations::subgraph::fetch::{self, SubgraphFetchInput}; +use rover_client::operations::subgraph::introspect::{self, SubgraphIntrospectInput}; +use rover_client::shared::{BuildError, GraphRef}; +use rover_client::RoverClientError; -use rover_client::operations::subgraph::fetch::SubgraphFetchInput; -use rover_client::operations::subgraph::introspect::SubgraphIntrospectInput; -use rover_client::shared::GraphRef; -use rover_client::{ - blocking::GraphQLClient, - operations::subgraph::{fetch, introspect}, -}; +use camino::Utf8PathBuf; +use harmonizer::ServiceDefinition as SubgraphDefinition; use serde::Serialize; -use std::{collections::HashMap, fs, str::FromStr}; use structopt::StructOpt; -use harmonizer::ServiceDefinition as SubgraphDefinition; +use std::{collections::HashMap, fs, str::FromStr}; #[derive(Debug, Serialize, StructOpt)] pub struct Compose { @@ -32,7 +29,7 @@ pub struct Compose { } impl Compose { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let supergraph_config = config::parse_supergraph_config(&self.config_path)?; let subgraph_definitions = get_subgraph_definitions( supergraph_config, @@ -42,24 +39,20 @@ impl Compose { )?; match harmonizer::harmonize(subgraph_definitions) { - Ok(core_schema) => Ok(RoverStdout::CoreSchema(core_schema)), - Err(composition_errors) => { - let num_failures = composition_errors.len(); - for composition_error in composition_errors { - eprintln!("{} {}", Red.bold().paint("error:"), &composition_error) - } - match num_failures { - 0 => unreachable!("Composition somehow failed with no composition errors."), - 1 => Err( - anyhow!("Encountered 1 composition error while composing the graph.") - .into(), - ), - _ => Err(anyhow!( - "Encountered {} composition errors while composing the graph.", - num_failures - ) - .into()), + Ok(core_schema) => Ok(RoverOutput::CoreSchema(core_schema)), + Err(harmonizer_composition_errors) => { + let mut build_errors = Vec::with_capacity(harmonizer_composition_errors.len()); + for harmonizer_composition_error in harmonizer_composition_errors { + if let Some(message) = &harmonizer_composition_error.message { + build_errors.push(BuildError::composition_error( + message.to_string(), + Some(harmonizer_composition_error.code().to_string()), + )); + } } + Err(RoverError::new(RoverClientError::BuildErrors { + source: build_errors.into(), + })) } } } diff --git a/src/command/supergraph/compose/no_compose.rs b/src/command/supergraph/compose/no_compose.rs index bc9c6904e..dea61423f 100644 --- a/src/command/supergraph/compose/no_compose.rs +++ b/src/command/supergraph/compose/no_compose.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use crate::utils::client::StudioClientConfig; use crate::{ anyhow, - command::RoverStdout, + command::RoverOutput, error::{RoverError, Suggestion}, Result, }; @@ -24,7 +24,7 @@ pub struct Compose { } impl Compose { - pub fn run(&self, _client_config: StudioClientConfig) -> Result { + pub fn run(&self, _client_config: StudioClientConfig) -> Result { let mut err = RoverError::new(anyhow!( "This version of Rover does not support this command." )); diff --git a/src/command/supergraph/fetch.rs b/src/command/supergraph/fetch.rs index 878b92bd5..793af1f87 100644 --- a/src/command/supergraph/fetch.rs +++ b/src/command/supergraph/fetch.rs @@ -1,5 +1,5 @@ use crate::utils::client::StudioClientConfig; -use crate::{command::RoverStdout, Result}; +use crate::{command::RoverOutput, Result}; use rover_client::operations::supergraph::fetch::{self, SupergraphFetchInput}; use rover_client::shared::GraphRef; @@ -23,7 +23,7 @@ pub struct Fetch { } impl Fetch { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -39,6 +39,6 @@ impl Fetch { &client, )?; - Ok(RoverStdout::FetchResponse(fetch_response)) + Ok(RoverOutput::FetchResponse(fetch_response)) } } diff --git a/src/command/supergraph/mod.rs b/src/command/supergraph/mod.rs index 8b575b5fa..51e2141a3 100644 --- a/src/command/supergraph/mod.rs +++ b/src/command/supergraph/mod.rs @@ -5,7 +5,7 @@ mod fetch; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -26,7 +26,7 @@ pub enum Command { } impl Supergraph { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { match &self.command { Command::Fetch(command) => command.run(client_config), Command::Compose(command) => command.run(client_config), diff --git a/src/command/update/check.rs b/src/command/update/check.rs index f4fe31ebd..3b73ab4ad 100644 --- a/src/command/update/check.rs +++ b/src/command/update/check.rs @@ -2,7 +2,7 @@ use reqwest::blocking::Client; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::{utils::version, Result}; use houston as config; @@ -13,8 +13,8 @@ pub struct Check { } impl Check { - pub fn run(&self, config: config::Config, client: Client) -> Result { + pub fn run(&self, config: config::Config, client: Client) -> Result { version::check_for_update(config, true, client)?; - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/update/mod.rs b/src/command/update/mod.rs index 9296ba172..8964ff90e 100644 --- a/src/command/update/mod.rs +++ b/src/command/update/mod.rs @@ -4,7 +4,7 @@ use reqwest::blocking::Client; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; use houston as config; @@ -22,7 +22,7 @@ pub enum Command { } impl Update { - pub fn run(&self, config: config::Config, client: Client) -> Result { + pub fn run(&self, config: config::Config, client: Client) -> Result { match &self.command { Command::Check(command) => command.run(config, client), } diff --git a/src/error/metadata/codes/E029.md b/src/error/metadata/codes/E029.md index 793b1022b..195333e5f 100644 --- a/src/error/metadata/codes/E029.md +++ b/src/error/metadata/codes/E029.md @@ -1,5 +1,5 @@ -This error occurs when you propose a subgraph schema that could not be composed. +This error occurs when you propose a subgraph schema that could not be built. -There are many reasons why you may run into composition errors. This error should include information about _why_ the proposed subgraph schema could not be composed. Error code references can be found [here](https://www.apollographql.com/docs/federation/errors/). +There are many reasons why you may run into build errors. This error should include information about _why_ the proposed subgraph schema could not be composed. Error code references can be found [here](https://www.apollographql.com/docs/federation/errors/). -Some composition errors are part of normal workflows. For instance, you may need to publish a subgraph that does not compose if you are trying to [migrate an entity or field](https://www.apollographql.com/docs/federation/entities/#migrating-entities-and-fields-advanced). +Some build errors are part of normal workflows. For instance, you may need to publish a subgraph that does not compose if you are trying to [migrate an entity or field](https://www.apollographql.com/docs/federation/entities/#migrating-entities-and-fields-advanced). diff --git a/src/error/metadata/mod.rs b/src/error/metadata/mod.rs index 8969d419d..39887b477 100644 --- a/src/error/metadata/mod.rs +++ b/src/error/metadata/mod.rs @@ -7,19 +7,23 @@ pub use suggestion::Suggestion; use houston::HoustonProblem; use rover_client::RoverClientError; -use crate::{command::output::print_check_response, utils::env::RoverEnvKey}; +use crate::utils::env::RoverEnvKey; use std::{env, fmt::Display}; -use ansi_term::Colour::Red; +use serde::Serialize; /// Metadata contains extra information about specific errors /// Currently this includes an optional error `Code` /// and an optional `Suggestion` -#[derive(Default, Debug)] +#[derive(Default, Serialize, Debug)] pub struct Metadata { + // skip serializing for now until we can appropriately strip color codes + #[serde(skip_serializing)] pub suggestion: Option, pub code: Option, + + #[serde(skip_serializing)] pub is_parse_error: bool, } @@ -58,33 +62,29 @@ impl From<&mut anyhow::Error> for Metadata { RoverClientError::InvalidSeverity => { (Some(Suggestion::SubmitIssue), Some(Code::E006)) } - RoverClientError::SubgraphCompositionErrors { + RoverClientError::SubgraphBuildErrors { graph_ref, - composition_errors, - } => { - for composition_error in composition_errors { - let error_descriptor = format!("{} ", Red.bold().paint("error:")); - eprintln!("{} {}", &error_descriptor, &composition_error); - } - ( - Some(Suggestion::FixSubgraphSchema { - graph_ref: graph_ref.clone(), - }), - Some(Code::E029), - ) + subgraph, + source: _, + } => ( + Some(Suggestion::FixSubgraphSchema { + graph_ref: graph_ref.clone(), + subgraph: subgraph.clone(), + }), + Some(Code::E029), + ), + RoverClientError::BuildErrors { .. } => { + (Some(Suggestion::FixCompositionErrors), Some(Code::E029)) } RoverClientError::OperationCheckFailure { graph_ref, - check_response, - } => { - print_check_response(check_response); - ( - Some(Suggestion::FixOperationsInSchema { - graph_ref: graph_ref.clone(), - }), - Some(Code::E030), - ) - } + check_response: _, + } => ( + Some(Suggestion::FixOperationsInSchema { + graph_ref: graph_ref.clone(), + }), + Some(Code::E030), + ), RoverClientError::SubgraphIntrospectionNotAvailable => { (Some(Suggestion::UseFederatedGraph), Some(Code::E007)) } @@ -129,13 +129,7 @@ impl From<&mut anyhow::Error> for Metadata { (Some(Suggestion::SubmitIssue), Some(Code::E015)) } RoverClientError::BadReleaseUrl => (Some(Suggestion::SubmitIssue), None), - RoverClientError::NoCompositionPublishes { - graph_ref: _, - composition_errors, - } => { - for composition_error in composition_errors { - eprintln!("{} {}", Red.bold().paint("error:"), composition_error); - } + RoverClientError::NoSupergraphBuilds { .. } => { (Some(Suggestion::RunComposition), Some(Code::E027)) } RoverClientError::AdhocError { .. } => (None, None), diff --git a/src/error/metadata/suggestion.rs b/src/error/metadata/suggestion.rs index 64cc73186..506166681 100644 --- a/src/error/metadata/suggestion.rs +++ b/src/error/metadata/suggestion.rs @@ -6,8 +6,10 @@ use rover_client::shared::GraphRef; use crate::utils::env::RoverEnvKey; +use serde::Serialize; + /// `Suggestion` contains possible suggestions for remedying specific errors. -#[derive(Debug)] +#[derive(Serialize, Debug)] pub enum Suggestion { SubmitIssue, SetConfigHome, @@ -34,7 +36,9 @@ pub enum Suggestion { CheckGnuVersion, FixSubgraphSchema { graph_ref: GraphRef, + subgraph: String, }, + FixCompositionErrors, FixOperationsInSchema { graph_ref: GraphRef, }, @@ -71,7 +75,7 @@ impl Display for Suggestion { ) } Suggestion::RunComposition => { - format!("Try resolving the composition errors in your subgraph(s), and publish them with the {} command.", Yellow.normal().paint("`rover subgraph publish`")) + format!("Try resolving the build errors in your subgraph(s), and publish them with the {} command.", Yellow.normal().paint("`rover subgraph publish`")) } Suggestion::UseFederatedGraph => { "Try running the command on a valid federated graph, or use the appropriate `rover graph` command instead of `rover subgraph`.".to_string() @@ -135,7 +139,8 @@ impl Display for Suggestion { Suggestion::CheckServerConnection => "Make sure the endpoint is accepting connections and is spelled correctly".to_string(), Suggestion::ConvertGraphToSubgraph => "If you are sure you want to convert a non-federated graph to a subgraph, you can re-run the same command with a `--convert` flag.".to_string(), Suggestion::CheckGnuVersion => "This is likely an issue with your current version of `glibc`. Try running `ldd --version`, and if the version >= 2.18, we suggest installing the Rover binary built for `x86_64-unknown-linux-gnu`".to_string(), - Suggestion::FixSubgraphSchema { graph_ref } => format!("The changes in the schema you proposed are incompatible with graph {}. See {} for more information on resolving composition errors.", Yellow.normal().paint(graph_ref.to_string()), Cyan.normal().paint("https://www.apollographql.com/docs/federation/errors/")), + Suggestion::FixSubgraphSchema { graph_ref, subgraph } => format!("The changes in the schema you proposed for subgraph {} are incompatible with supergraph {}. See {} for more information on resolving build errors.", Yellow.normal().paint(subgraph.to_string()), Yellow.normal().paint(graph_ref.to_string()), Cyan.normal().paint("https://www.apollographql.com/docs/federation/errors/")), + Suggestion::FixCompositionErrors => format!("The subgraph schemas you provided are incompatible with each other. See {} for more information on resolving build errors.", Cyan.normal().paint("https://www.apollographql.com/docs/federation/errors/")), Suggestion::FixOperationsInSchema { graph_ref } => format!("The changes in the schema you proposed are incompatible with graph {}. See {} for more information on resolving operation check errors.", Yellow.normal().paint(graph_ref.to_string()), Cyan.normal().paint("https://www.apollographql.com/docs/studio/schema-checks/")) }; write!(formatter, "{}", &suggestion) diff --git a/src/error/mod.rs b/src/error/mod.rs index 1da93d72b..dde8b47a1 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -6,21 +6,64 @@ pub(crate) use metadata::Metadata; pub type Result = std::result::Result; use ansi_term::Colour::{Cyan, Red}; +use rover_client::RoverClientError; +use serde::ser::SerializeStruct; +use serde::{Serialize, Serializer}; +use serde_json::{json, Value}; use std::borrow::BorrowMut; +use std::error::Error; use std::fmt::{self, Debug, Display}; pub use self::metadata::Suggestion; +use rover_client::shared::BuildErrors; + /// A specialized `Error` type for Rover that wraps `anyhow` /// and provides some extra `Metadata` for end users depending /// on the specific error they encountered. -#[derive(Debug)] +#[derive(Serialize, Debug)] pub struct RoverError { + #[serde(flatten, serialize_with = "serialize_anyhow")] error: anyhow::Error, + + #[serde(flatten)] metadata: Metadata, } +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +enum RoverDetails { + BuildErrors(BuildErrors), +} + +fn serialize_anyhow(error: &anyhow::Error, serializer: S) -> std::result::Result +where + S: Serializer, +{ + let top_level_struct = "error"; + let message_field_name = "message"; + let details_struct = "details"; + + if let Some(rover_client_error) = error.downcast_ref::() { + if let Some(rover_client_error_source) = rover_client_error.source() { + if let Some(build_errors) = rover_client_error_source.downcast_ref::() { + let mut top_level_data = serializer.serialize_struct(top_level_struct, 2)?; + top_level_data.serialize_field(message_field_name, &error.to_string())?; + top_level_data.serialize_field( + details_struct, + &RoverDetails::BuildErrors(build_errors.clone()), + )?; + return top_level_data.end(); + } + } + } + + let mut data = serializer.serialize_struct(top_level_struct, 1)?; + data.serialize_field(message_field_name, &error.to_string())?; + data.end() +} + impl RoverError { pub fn new(error: E) -> Self where @@ -49,6 +92,33 @@ impl RoverError { pub fn suggestion(&mut self) -> &Option { &self.metadata.suggestion } + + pub fn print(&self) { + if let Some(RoverClientError::OperationCheckFailure { + graph_ref: _, + check_response, + }) = self.error.downcast_ref::() + { + println!("{}", check_response.get_table()); + } + + eprintln!("{}", self); + } + + pub(crate) fn get_internal_data_json(&self) -> Value { + if let Some(RoverClientError::OperationCheckFailure { + graph_ref: _, + check_response, + }) = self.error.downcast_ref::() + { + return check_response.get_json(); + } + Value::Null + } + + pub(crate) fn get_internal_error_json(&self) -> Value { + json!(self) + } } impl Display for RoverError { @@ -64,7 +134,7 @@ impl Display for RoverError { "error:".to_string() }; let error_descriptor = Red.bold().paint(&error_descriptor_message); - writeln!(formatter, "{} {}", error_descriptor, &self.error)?; + writeln!(formatter, "{} {:?}", error_descriptor, &self.error)?; error_descriptor_message }; diff --git a/src/utils/stringify.rs b/src/utils/stringify.rs index 6bd57471e..3d45e594c 100644 --- a/src/utils/stringify.rs +++ b/src/utils/stringify.rs @@ -2,20 +2,30 @@ //! a struct with the `Display`/`FromStr` implementations //! if it does not implement `Serialize`/`Deserialize` //! code taken from this: https://github.com/serde-rs/serde/issues/1316 -//! and can be used by annotating a field with -//! #[serde(serialize_with = "from_display")] +//! and can be used by annotating a field with either +//! #[serde(serialize_with = "from_display")] or +//! #[serde(serialize_with = "option_from_display")] +//! depending on if the type you're serializing is nested in an Option use std::fmt::Display; use serde::Serializer; -pub fn from_display(value: &Option, serializer: S) -> Result +pub fn option_from_display(value: &Option, serializer: S) -> Result where T: Display, S: Serializer, { if let Some(value) = value { - serializer.collect_str(value) + from_display(value, serializer) } else { serializer.serialize_none() } } + +pub fn from_display(value: &T, serializer: S) -> Result +where + T: Display, + S: Serializer, +{ + serializer.collect_str(value) +} diff --git a/src/utils/table.rs b/src/utils/table.rs index c19ad7778..1b098c1ec 100644 --- a/src/utils/table.rs +++ b/src/utils/table.rs @@ -1,9 +1,16 @@ -use prettytable::{format::consts::FORMAT_BOX_CHARS, Table}; +use prettytable::{ + format::{consts::FORMAT_BOX_CHARS, TableFormat}, + Table, +}; pub use prettytable::{cell, row}; pub fn get_table() -> Table { let mut table = Table::new(); - table.set_format(*FORMAT_BOX_CHARS); + table.set_format(get_table_format()); table } + +pub fn get_table_format() -> TableFormat { + *FORMAT_BOX_CHARS +}