Skip to content

Commit 716c278

Browse files
wip: refactor subgraph check and add line numbers
1 parent c1b08d9 commit 716c278

File tree

14 files changed

+191
-143
lines changed

14 files changed

+191
-143
lines changed

crates/rover-client/src/error.rs

+21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use reqwest::Url;
22
use thiserror::Error;
33

4+
use crate::query::subgraph::check::types::CompositionError;
5+
46
/// RoverClientError represents all possible failures that can occur during a client request.
57
#[derive(Error, Debug)]
68
pub enum RoverClientError {
@@ -103,6 +105,12 @@ pub enum RoverClientError {
103105
graph: String,
104106
composition_errors: Vec<String>,
105107
},
108+
109+
#[error("{}", subgraph_composition_error_msg(.composition_errors))]
110+
SubgraphCompositionErrors {
111+
graph_name: String,
112+
composition_errors: Vec<CompositionError>
113+
},
106114

107115
/// This error occurs when the Studio API returns no implementing services for a graph
108116
/// This response shouldn't be possible!
@@ -141,3 +149,16 @@ pub enum RoverClientError {
141149
#[error("This endpoint doesn't support subgraph introspection via the Query._service field")]
142150
SubgraphIntrospectionNotAvailable,
143151
}
152+
153+
fn subgraph_composition_error_msg(composition_errors: &[CompositionError]) -> String {
154+
let num_failures = composition_errors.len();
155+
if num_failures == 0 {
156+
unreachable!("No composition errors were encountered while composing the supergraph.");
157+
}
158+
let mut msg = String::new();
159+
msg.push_str(&match num_failures {
160+
1 => "Encountered 1 composition error while composing the supergraph.".to_string(),
161+
_ => format!("Encountered {} composition errors while composing the supergraph.", num_failures)
162+
});
163+
msg
164+
}

crates/rover-client/src/query/subgraph/check.graphql crates/rover-client/src/query/subgraph/check/check_query.graphql

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mutation CheckPartialSchemaQuery(
1+
mutation SubgraphCheckQuery(
22
$graph_id: ID!
33
$variant: String!
44
$implementingServiceName: String!
@@ -17,6 +17,11 @@
1717
compositionValidationResult {
1818
errors {
1919
message
20+
code
21+
locations {
22+
line
23+
column
24+
}
2025
}
2126
}
2227
checkSchemaResult {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod query_runner;
2+
pub(crate) mod types;
3+
pub use types::SubgraphCheckResponse;
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,31 @@
1+
use graphql_client::*;
12
use crate::blocking::StudioClient;
23
use crate::query::config::is_federated;
34
use crate::RoverClientError;
5+
use super::types::*;
46

5-
use graphql_client::*;
6-
7-
use reqwest::Url;
8-
9-
type Timestamp = String;
107
#[derive(GraphQLQuery)]
118
// The paths are relative to the directory where your `Cargo.toml` is located.
129
// Both json and the GraphQL schema language are supported as sources for the schema
1310
#[graphql(
14-
query_path = "src/query/subgraph/check.graphql",
15-
schema_path = ".schema/schema.graphql",
16-
response_derives = "PartialEq, Debug, Serialize, Deserialize",
17-
deprecated = "warn"
11+
query_path = "src/query/subgraph/check/check_query.graphql",
12+
schema_path = ".schema/schema.graphql",
13+
response_derives = "PartialEq, Debug, Serialize, Deserialize",
14+
deprecated = "warn"
1815
)]
1916
/// This struct is used to generate the module containing `Variables` and
2017
/// `ResponseData` structs.
21-
/// Snake case of this name is the mod name. i.e. check_partial_schema_query
22-
pub struct CheckPartialSchemaQuery;
18+
/// Snake case of this name is the mod name. i.e. subgraph_check_query
19+
pub struct SubgraphCheckQuery;
20+
2321

2422
/// The main function to be used from this module.
2523
/// This function takes a proposed schema and validates it against a published
2624
/// schema.
2725
pub fn run(
28-
variables: check_partial_schema_query::Variables,
26+
variables: subgraph_check_query::Variables,
2927
client: &StudioClient,
30-
) -> Result<CheckResponse, RoverClientError> {
28+
) -> Result<SubgraphCheckResponse, RoverClientError> {
3129
let graph = variables.graph_id.clone();
3230
// This response is used to check whether or not the current graph is federated.
3331
let is_federated = is_federated::run(
@@ -43,77 +41,67 @@ pub fn run(
4341
can_operation_convert: false,
4442
});
4543
}
46-
let data = client.post::<CheckPartialSchemaQuery>(variables)?;
44+
let data = client.post::<SubgraphCheckQuery>(variables)?;
4745
get_check_response_from_data(data, graph)
4846
}
4947

50-
pub enum CheckResponse {
51-
CompositionErrors(Vec<check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCompositionValidationResultErrors>),
52-
CheckResult(CheckResult)
53-
}
54-
55-
#[derive(Debug)]
56-
pub struct CheckResult {
57-
pub target_url: Option<Url>,
58-
pub number_of_checked_operations: i64,
59-
pub change_severity: check_partial_schema_query::ChangeSeverity,
60-
pub changes: Vec<check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCheckSchemaResultDiffToPreviousChanges>,
61-
}
62-
6348
fn get_check_response_from_data(
64-
data: check_partial_schema_query::ResponseData,
65-
graph: String,
66-
) -> Result<CheckResponse, RoverClientError> {
67-
let service = data.service.ok_or(RoverClientError::NoService { graph })?;
49+
data: subgraph_check_query::ResponseData,
50+
graph_name: String,
51+
) -> Result<SubgraphCheckResponse, RoverClientError> {
52+
let service = data.service.ok_or(RoverClientError::NoService { graph: graph_name.clone() })?;
6853

6954
// for some reason this is a `Vec<Option<CompositionError>>`
7055
// we convert this to just `Vec<CompositionError>` because the `None`
7156
// errors would be useless.
72-
let composition_errors: Vec<check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCompositionValidationResultErrors> = service
57+
let query_composition_errors: Vec<subgraph_check_query::SubgraphCheckQueryServiceCheckPartialSchemaCompositionValidationResultErrors> = service
7358
.check_partial_schema
7459
.composition_validation_result
7560
.errors;
7661

77-
if composition_errors.is_empty() {
62+
if query_composition_errors.is_empty() {
7863
let check_schema_result = service.check_partial_schema.check_schema_result.ok_or(
7964
RoverClientError::MalformedResponse {
8065
null_field: "service.check_partial_schema.check_schema_result".to_string(),
8166
},
8267
)?;
8368

84-
let target_url = get_url(check_schema_result.target_url);
69+
let target_url = check_schema_result.target_url.map(|u| u.to_string());
8570

8671
let diff_to_previous = check_schema_result.diff_to_previous;
8772

8873
let number_of_checked_operations =
8974
diff_to_previous.number_of_checked_operations.unwrap_or(0);
9075

91-
let change_severity = diff_to_previous.severity;
92-
let changes = diff_to_previous.changes;
76+
let change_severity = diff_to_previous.severity.into();
77+
78+
let mut changes = Vec::with_capacity(diff_to_previous.changes.len());
79+
for change in diff_to_previous.changes {
80+
changes.push(SchemaChange {
81+
code: change.code,
82+
severity: change.severity.into(),
83+
description: change.description
84+
});
85+
}
9386

94-
let check_result = CheckResult {
87+
let check_result = SubgraphCheckResponse {
9588
target_url,
9689
number_of_checked_operations,
97-
change_severity,
9890
changes,
91+
change_severity
9992
};
10093

101-
Ok(CheckResponse::CheckResult(check_result))
94+
Ok(check_result)
10295
} else {
103-
Ok(CheckResponse::CompositionErrors(composition_errors))
104-
}
105-
}
106-
107-
fn get_url(url: Option<String>) -> Option<Url> {
108-
match url {
109-
Some(url) => {
110-
let url = Url::parse(&url);
111-
match url {
112-
Ok(url) => Some(url),
113-
// if the API returns an invalid URL, don't put it in the response
114-
Err(_) => None,
115-
}
96+
let num_failures = query_composition_errors.len();
97+
98+
let mut composition_errors = Vec::with_capacity(num_failures);
99+
for query_composition_error in query_composition_errors {
100+
composition_errors.push(CompositionError {
101+
message: query_composition_error.message,
102+
code: query_composition_error.code
103+
});
116104
}
117-
None => None,
105+
Err(RoverClientError::SubgraphCompositionErrors { graph_name, composition_errors })
118106
}
119107
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use std::fmt;
2+
3+
use super::query_runner::subgraph_check_query;
4+
5+
pub(crate) type Timestamp = String;
6+
type QueryChangeSeverity = subgraph_check_query::ChangeSeverity;
7+
8+
#[derive(Debug, Clone, PartialEq)]
9+
pub struct SubgraphCheckResponse {
10+
pub target_url: Option<String>,
11+
pub number_of_checked_operations: i64,
12+
pub changes: Vec<SchemaChange>,
13+
pub change_severity: ChangeSeverity
14+
}
15+
16+
#[derive(Debug, Clone, PartialEq)]
17+
pub enum ChangeSeverity {
18+
PASS,
19+
FAIL,
20+
}
21+
22+
impl fmt::Display for ChangeSeverity {
23+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24+
let msg = match self {
25+
ChangeSeverity::PASS => "PASS",
26+
ChangeSeverity::FAIL => "FAIL"
27+
};
28+
write!(f, "{}", msg)
29+
}
30+
}
31+
32+
impl From<QueryChangeSeverity> for ChangeSeverity {
33+
fn from(severity: QueryChangeSeverity) -> Self {
34+
match severity {
35+
QueryChangeSeverity::NOTICE => ChangeSeverity::PASS,
36+
QueryChangeSeverity::FAILURE => ChangeSeverity::FAIL,
37+
_ => unreachable!("Unknown change severity")
38+
}
39+
}
40+
}
41+
42+
#[derive(Debug, Clone, PartialEq)]
43+
pub struct SchemaChange {
44+
pub code: String,
45+
pub description: String,
46+
pub severity: ChangeSeverity
47+
}
48+
49+
#[derive(Debug, Clone, PartialEq)]
50+
pub struct CompositionError {
51+
pub message: String,
52+
pub code: Option<String>
53+
}

docs/source/errors.md

+8
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,12 @@ This error occurs when Rover could not connect to an HTTP endpoint.
233233

234234
If you encountered this error while running introspection, you'll want to make sure that you typed the endpoint correctly, your Internet connection is stable, and that your server is responding to requests. You may wish to run the command again with `--log=debug`.
235235

236+
### E029
237+
238+
This error occurs when you propose a subgraph schema that could not be composed.
239+
240+
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/).
241+
242+
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).
243+
236244

src/command/output.rs

+32-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::utils::table::{self, cell, row};
55
use ansi_term::{Colour::Yellow, Style};
66
use atty::Stream;
77
use crossterm::style::Attribute::Underlined;
8-
use rover_client::query::subgraph::list::ListDetails;
8+
use rover_client::query::subgraph::{check::SubgraphCheckResponse, list::ListDetails};
99
use termimad::MadSkin;
1010

1111
/// RoverStdout defines all of the different types of data that are printed
@@ -24,6 +24,7 @@ pub enum RoverStdout {
2424
CoreSchema(String),
2525
SchemaHash(String),
2626
SubgraphList(ListDetails),
27+
SubgraphCheck(SubgraphCheckResponse),
2728
VariantList(Vec<String>),
2829
Profiles(Vec<String>),
2930
Introspection(String),
@@ -97,6 +98,36 @@ impl RoverStdout {
9798
details.root_url, details.graph_name
9899
);
99100
}
101+
RoverStdout::SubgraphCheck(check_response) => {
102+
let num_changes = check_response.changes.len();
103+
104+
let msg = match num_changes {
105+
0 => "There were no changes detected in the composed schema.".to_string(),
106+
_ => format!(
107+
"Compared {} schema changes against {} operations",
108+
check_response.changes.len(),
109+
check_response.number_of_checked_operations
110+
),
111+
};
112+
113+
eprintln!("{}", &msg);
114+
115+
if !check_response.changes.is_empty() {
116+
let mut table = table::get_table();
117+
118+
// bc => sets top row to be bold and center
119+
table.add_row(row![bc => "Change", "Code", "Description"]);
120+
for check in &check_response.changes {
121+
table.add_row(row![check.severity, check.code, check.description]);
122+
}
123+
124+
print_content(table.to_string());
125+
}
126+
127+
if let Some(url) = &check_response.target_url {
128+
eprintln!("View full details at {}", url);
129+
}
130+
}
100131
RoverStdout::VariantList(variants) => {
101132
print_descriptor("Variants");
102133
for variant in variants {

0 commit comments

Comments
 (0)