Skip to content

Commit e0f42c0

Browse files
chore: refactor subgraph check
This commit does a lot of heavy lifting for the pending rebase. 1) Creates new input and output types in rover-client for subgraph check 2) Moves GitContext out of rover::utils to rover-client::utils 3) Creates error code E029 for composition errors 4) Styles cloud-composition errors like harmonizer
1 parent dea8435 commit e0f42c0

File tree

25 files changed

+385
-260
lines changed

25 files changed

+385
-260
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rover-client/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ houston = {path = "../houston"}
1414
anyhow = "1"
1515
camino = "1"
1616
chrono = "0.4"
17+
git-url-parse = "0.3.1"
18+
git2 = { version = "0.13.20", default-features = false, features = ["vendored-openssl"] }
1719
graphql-parser = "0.3.0"
1820
graphql_client = "0.9"
1921
http = "0.2"

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/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ pub mod query;
2121

2222
/// Module for getting release info
2323
pub mod releases;
24+
25+
/// Module for shared functionality
26+
pub mod utils;

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

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
1-
mutation CheckPartialSchemaQuery(
1+
mutation SubgraphCheckQuery(
22
$graph_id: ID!
33
$variant: String!
4-
$implementingServiceName: String!
5-
$partialSchema: PartialSchemaInput!
6-
$gitContext: GitContextInput!
4+
$subgraph: String!
5+
$proposed_schema: PartialSchemaInput!
6+
$git_context: GitContextInput!
77
$config: HistoricQueryParameters!
88
) {
99
service(id: $graph_id) {
1010
checkPartialSchema(
1111
graphVariant: $variant
12-
implementingServiceName: $implementingServiceName
13-
partialSchema: $partialSchema
14-
gitContext: $gitContext
12+
implementingServiceName: $subgraph
13+
partialSchema: $proposed_schema
14+
gitContext: $git_context
1515
historicParameters: $config
1616
) {
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::{SubgraphCheckConfig, SubgraphCheckInput, SubgraphCheckResponse};
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,36 @@
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+
input: SubgraphCheckInput,
2926
client: &StudioClient,
30-
) -> Result<CheckResponse, RoverClientError> {
31-
let graph = variables.graph_id.clone();
27+
) -> Result<SubgraphCheckResponse, RoverClientError> {
28+
let graph = input.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(
3431
is_federated::is_federated_graph::Variables {
35-
graph_id: variables.graph_id.clone(),
36-
graph_variant: variables.variant.clone(),
32+
graph_id: input.graph_id.clone(),
33+
graph_variant: input.variant.clone(),
3734
},
3835
&client,
3936
)?;
@@ -43,77 +40,71 @@ pub fn run(
4340
can_operation_convert: false,
4441
});
4542
}
46-
let data = client.post::<CheckPartialSchemaQuery>(variables)?;
43+
let variables = input.into();
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 {
53+
graph: graph_name.clone(),
54+
})?;
6855

6956
// for some reason this is a `Vec<Option<CompositionError>>`
7057
// we convert this to just `Vec<CompositionError>` because the `None`
7158
// errors would be useless.
72-
let composition_errors: Vec<check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCompositionValidationResultErrors> = service
59+
let query_composition_errors: Vec<subgraph_check_query::SubgraphCheckQueryServiceCheckPartialSchemaCompositionValidationResultErrors> = service
7360
.check_partial_schema
7461
.composition_validation_result
7562
.errors;
7663

77-
if composition_errors.is_empty() {
64+
if query_composition_errors.is_empty() {
7865
let check_schema_result = service.check_partial_schema.check_schema_result.ok_or(
7966
RoverClientError::MalformedResponse {
8067
null_field: "service.check_partial_schema.check_schema_result".to_string(),
8168
},
8269
)?;
8370

84-
let target_url = get_url(check_schema_result.target_url);
85-
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();
9377

94-
let check_result = CheckResult {
95-
target_url,
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+
}
86+
87+
let check_result = SubgraphCheckResponse {
88+
target_url: check_schema_result.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 {
106+
graph_name,
107+
composition_errors,
108+
})
118109
}
119110
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use std::fmt;
2+
3+
use crate::utils::GitContext;
4+
5+
use super::query_runner::subgraph_check_query;
6+
7+
pub(crate) type Timestamp = String;
8+
type QueryVariables = subgraph_check_query::Variables;
9+
type QueryChangeSeverity = subgraph_check_query::ChangeSeverity;
10+
type QuerySchema = subgraph_check_query::PartialSchemaInput;
11+
type QueryConfig = subgraph_check_query::HistoricQueryParameters;
12+
13+
#[derive(Debug, Clone, PartialEq)]
14+
pub struct SubgraphCheckInput {
15+
pub graph_id: String,
16+
pub variant: String,
17+
pub subgraph: String,
18+
pub proposed_schema: String,
19+
pub git_context: GitContext,
20+
pub config: SubgraphCheckConfig,
21+
}
22+
23+
#[derive(Debug, Clone, PartialEq)]
24+
pub struct SubgraphCheckConfig {
25+
pub query_count_threshold: Option<i64>,
26+
pub query_count_threshold_percentage: Option<f64>,
27+
pub validation_period_from: Option<String>,
28+
pub validation_period_to: Option<String>,
29+
}
30+
31+
impl From<SubgraphCheckInput> for QueryVariables {
32+
fn from(input: SubgraphCheckInput) -> Self {
33+
Self {
34+
graph_id: input.graph_id,
35+
variant: input.variant,
36+
subgraph: input.subgraph,
37+
proposed_schema: QuerySchema {
38+
sdl: Some(input.proposed_schema),
39+
hash: None,
40+
},
41+
config: QueryConfig {
42+
query_count_threshold: input.config.query_count_threshold,
43+
query_count_threshold_percentage: input.config.query_count_threshold_percentage,
44+
from: input.config.validation_period_from,
45+
to: input.config.validation_period_to,
46+
// we don't support configuring these, but we can't leave them out
47+
excluded_clients: None,
48+
ignored_operations: None,
49+
included_variants: None,
50+
},
51+
git_context: input.git_context.into(),
52+
}
53+
}
54+
}
55+
56+
#[derive(Debug, Clone, PartialEq)]
57+
pub struct SubgraphCheckResponse {
58+
pub target_url: Option<String>,
59+
pub number_of_checked_operations: i64,
60+
pub changes: Vec<SchemaChange>,
61+
pub change_severity: ChangeSeverity,
62+
}
63+
64+
#[derive(Debug, Clone, PartialEq)]
65+
pub enum ChangeSeverity {
66+
PASS,
67+
FAIL,
68+
}
69+
70+
impl fmt::Display for ChangeSeverity {
71+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72+
let msg = match self {
73+
ChangeSeverity::PASS => "PASS",
74+
ChangeSeverity::FAIL => "FAIL",
75+
};
76+
write!(f, "{}", msg)
77+
}
78+
}
79+
80+
impl From<QueryChangeSeverity> for ChangeSeverity {
81+
fn from(severity: QueryChangeSeverity) -> Self {
82+
match severity {
83+
QueryChangeSeverity::NOTICE => ChangeSeverity::PASS,
84+
QueryChangeSeverity::FAILURE => ChangeSeverity::FAIL,
85+
_ => unreachable!("Unknown change severity"),
86+
}
87+
}
88+
}
89+
90+
#[derive(Debug, Clone, PartialEq)]
91+
pub struct SchemaChange {
92+
pub code: String,
93+
pub description: String,
94+
pub severity: ChangeSeverity,
95+
}
96+
97+
#[derive(Debug, Clone, PartialEq)]
98+
pub struct CompositionError {
99+
pub message: String,
100+
pub code: Option<String>,
101+
}

0 commit comments

Comments
 (0)