Skip to content

Commit 382b668

Browse files
wip: refactor subgraph check and add line numbers
1 parent 3c85923 commit 382b668

File tree

14 files changed

+214
-141
lines changed

14 files changed

+214
-141
lines changed

crates/rover-client/src/error.rs

+24
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 {
@@ -104,6 +106,12 @@ pub enum RoverClientError {
104106
composition_errors: Vec<String>,
105107
},
106108

109+
#[error("{}", subgraph_composition_error_msg(.composition_errors))]
110+
SubgraphCompositionErrors {
111+
graph_name: String,
112+
composition_errors: Vec<CompositionError>,
113+
},
114+
107115
/// This error occurs when the Studio API returns no implementing services for a graph
108116
/// This response shouldn't be possible!
109117
#[error("The response from Apollo Studio was malformed. Response body contains `null` value for \"{null_field}\"")]
@@ -141,3 +149,19 @@ 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!(
162+
"Encountered {} composition errors while composing the supergraph.",
163+
num_failures
164+
),
165+
});
166+
msg
167+
}

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,30 @@
1+
use super::types::*;
12
use crate::blocking::StudioClient;
23
use crate::query::config::is_federated;
34
use crate::RoverClientError;
4-
55
use graphql_client::*;
66

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",
11+
query_path = "src/query/subgraph/check/check_query.graphql",
1512
schema_path = ".schema/schema.graphql",
1613
response_derives = "PartialEq, Debug, Serialize, Deserialize",
1714
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;
2320

2421
/// The main function to be used from this module.
2522
/// This function takes a proposed schema and validates it against a published
2623
/// schema.
2724
pub fn run(
28-
variables: check_partial_schema_query::Variables,
25+
variables: subgraph_check_query::Variables,
2926
client: &StudioClient,
30-
) -> Result<CheckResponse, RoverClientError> {
27+
) -> Result<SubgraphCheckResponse, RoverClientError> {
3128
let graph = variables.graph_id.clone();
3229
// This response is used to check whether or not the current graph is federated.
3330
let is_federated = is_federated::run(
@@ -43,77 +40,70 @@ pub fn run(
4340
can_operation_convert: false,
4441
});
4542
}
46-
let data = client.post::<CheckPartialSchemaQuery>(variables)?;
43+
let data = client.post::<SubgraphCheckQuery>(variables)?;
4744
get_check_response_from_data(data, graph)
4845
}
4946

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-
6347
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 })?;
48+
data: subgraph_check_query::ResponseData,
49+
graph_name: String,
50+
) -> Result<SubgraphCheckResponse, RoverClientError> {
51+
let service = data.service.ok_or(RoverClientError::NoService {
52+
graph: graph_name.clone(),
53+
})?;
6854

6955
// for some reason this is a `Vec<Option<CompositionError>>`
7056
// we convert this to just `Vec<CompositionError>` because the `None`
7157
// errors would be useless.
72-
let composition_errors: Vec<check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCompositionValidationResultErrors> = service
58+
let query_composition_errors: Vec<subgraph_check_query::SubgraphCheckQueryServiceCheckPartialSchemaCompositionValidationResultErrors> = service
7359
.check_partial_schema
7460
.composition_validation_result
7561
.errors;
7662

77-
if composition_errors.is_empty() {
63+
if query_composition_errors.is_empty() {
7864
let check_schema_result = service.check_partial_schema.check_schema_result.ok_or(
7965
RoverClientError::MalformedResponse {
8066
null_field: "service.check_partial_schema.check_schema_result".to_string(),
8167
},
8268
)?;
8369

84-
let target_url = get_url(check_schema_result.target_url);
85-
8670
let diff_to_previous = check_schema_result.diff_to_previous;
8771

8872
let number_of_checked_operations =
8973
diff_to_previous.number_of_checked_operations.unwrap_or(0);
9074

91-
let change_severity = diff_to_previous.severity;
92-
let changes = diff_to_previous.changes;
75+
let change_severity = diff_to_previous.severity.into();
9376

94-
let check_result = CheckResult {
95-
target_url,
77+
let mut changes = Vec::with_capacity(diff_to_previous.changes.len());
78+
for change in diff_to_previous.changes {
79+
changes.push(SchemaChange {
80+
code: change.code,
81+
severity: change.severity.into(),
82+
description: change.description,
83+
});
84+
}
85+
86+
let check_result = SubgraphCheckResponse {
87+
target_url: check_schema_result.target_url,
9688
number_of_checked_operations,
97-
change_severity,
9889
changes,
90+
change_severity,
9991
};
10092

101-
Ok(CheckResponse::CheckResult(check_result))
93+
Ok(check_result)
10294
} 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-
}
95+
let num_failures = query_composition_errors.len();
96+
97+
let mut composition_errors = Vec::with_capacity(num_failures);
98+
for query_composition_error in query_composition_errors {
99+
composition_errors.push(CompositionError {
100+
message: query_composition_error.message,
101+
code: query_composition_error.code,
102+
});
116103
}
117-
None => None,
104+
Err(RoverClientError::SubgraphCompositionErrors {
105+
graph_name,
106+
composition_errors,
107+
})
118108
}
119109
}
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)