Skip to content

Commit 8cd0e88

Browse files
feat(rover): graph checks
1 parent 0b9ee37 commit 8cd0e88

File tree

9 files changed

+460
-159
lines changed

9 files changed

+460
-159
lines changed

Cargo.lock

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

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ binstall = { path = "./installers/binstall" }
1919
anyhow = "1.0.31"
2020
atty = "0.2.14"
2121
console = "0.13.0"
22+
prettytable-rs = "0.8.0"
2223
serde = "1.0"
2324
serde_json = "1.0"
2425
structopt = "0.3.15"
25-
url = "2.1.1"
2626
tracing = "0.1.21"
2727
regex = "1"
28+
url = "2.1.1"
2829

2930
[dev-dependencies]
3031
assert_cmd = "1.0.1"

crates/rover-client/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ reqwest = { version = "0.10", features = ["json", "blocking", "native-tls-vendor
1313
serde = "1"
1414
serde_json = "1"
1515
thiserror = "1"
16+
tracing = "0.1"
1617

1718
[build-dependencies]
1819
online = "0.2.2"

crates/rover-client/src/error.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ pub enum RoverClientError {
1515
#[error("invalid header value")]
1616
InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
1717

18+
/// Invalid JSON in response body.
19+
#[error("could not parse JSON")]
20+
InvalidJSON(#[from] serde_json::Error),
21+
1822
/// Encountered an error handling the received response.
1923
#[error("encountered an error handling the response: {msg}")]
2024
HandleResponse {
@@ -32,9 +36,13 @@ pub enum RoverClientError {
3236
#[error("The response from the server was malformed. There was no data found in the reponse body. This is likely an error in GraphQL execution")]
3337
NoData,
3438

35-
/// when someone provides a bad service/variant combingation or isn't
39+
/// when someone provides a bad service/variant combination or isn't
3640
/// validated properly, we don't know which reason is at fault for data.service
3741
/// being empty, so this error tells them to check both.
3842
#[error("No service found. Either the service/variant combination wasn't found or your API key is invalid.")]
3943
NoService,
44+
45+
/// The API returned an invalid ChangeSeverity value
46+
#[error("Invalid ChangeSeverity.")]
47+
InvalidSeverity,
4048
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
mutation CheckSchemaQuery(
2+
$graphId: ID!
3+
$variant: String
4+
$schema: String
5+
) {
6+
service(id: $graphId) {
7+
checkSchema(
8+
proposedSchemaDocument: $schema
9+
baseSchemaTag: $variant
10+
) {
11+
targetUrl
12+
diffToPrevious {
13+
severity
14+
numberOfCheckedOperations
15+
changes {
16+
severity
17+
code
18+
description
19+
}
20+
}
21+
}
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use crate::blocking::StudioClient;
2+
use crate::RoverClientError;
3+
use graphql_client::*;
4+
5+
use reqwest::Url;
6+
7+
#[derive(GraphQLQuery)]
8+
// The paths are relative to the directory where your `Cargo.toml` is located.
9+
// Both json and the GraphQL schema language are supported as sources for the schema
10+
#[graphql(
11+
query_path = "src/query/graph/check.graphql",
12+
schema_path = ".schema/schema.graphql",
13+
response_derives = "PartialEq, Debug, Serialize, Deserialize",
14+
deprecated = "warn"
15+
)]
16+
/// This struct is used to generate the module containing `Variables` and
17+
/// `ResponseData` structs.
18+
/// Snake case of this name is the mod name. i.e. check_schema_query
19+
pub struct CheckSchemaQuery;
20+
21+
/// The main function to be used from this module.
22+
/// This function takes a proposed schema and validates it against a pushed
23+
/// schema.
24+
pub fn run(
25+
variables: check_schema_query::Variables,
26+
client: &StudioClient,
27+
) -> Result<CheckResponse, RoverClientError> {
28+
let data = client.post::<CheckSchemaQuery>(variables)?;
29+
get_check_response_from_data(data)
30+
}
31+
32+
#[derive(Debug)]
33+
pub struct CheckResponse {
34+
pub target_url: Option<Url>,
35+
pub number_of_checked_operations: i64,
36+
pub change_severity: check_schema_query::ChangeSeverity,
37+
pub changes: Vec<check_schema_query::CheckSchemaQueryServiceCheckSchemaDiffToPreviousChanges>,
38+
}
39+
40+
fn get_check_response_from_data(
41+
data: check_schema_query::ResponseData,
42+
) -> Result<CheckResponse, RoverClientError> {
43+
let service = data.service.ok_or(RoverClientError::NoService)?;
44+
let target_url = get_url(service.check_schema.target_url);
45+
46+
let diff_to_previous = service.check_schema.diff_to_previous;
47+
48+
let number_of_checked_operations = diff_to_previous.number_of_checked_operations.unwrap_or(0);
49+
50+
let change_severity = diff_to_previous.severity;
51+
let changes = diff_to_previous.changes;
52+
53+
Ok(CheckResponse {
54+
target_url,
55+
number_of_checked_operations,
56+
change_severity,
57+
changes,
58+
})
59+
}
60+
61+
fn get_url(url: Option<String>) -> Option<Url> {
62+
match url {
63+
Some(url) => {
64+
let url = Url::parse(&url);
65+
match url {
66+
Ok(url) => Some(url),
67+
// if the API returns an invalid URL, don't put it in the response
68+
Err(_) => None,
69+
}
70+
}
71+
None => None,
72+
}
73+
}
+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
/// "graph get" command execution
1+
/// "graph fetch" command execution
22
pub mod fetch;
33

44
/// "graph push" command execution
55
pub mod push;
6+
7+
/// "graph check" command exeuction
8+
pub mod check;

src/command/graph/check.rs

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use std::path::PathBuf;
2+
3+
use anyhow::{Context, Result};
4+
use prettytable::{cell, row, Table};
5+
use serde::Serialize;
6+
use structopt::StructOpt;
7+
8+
use rover_client::query::graph::check;
9+
10+
use crate::client::get_studio_client;
11+
use crate::command::RoverStdout;
12+
use crate::utils::parsers::{parse_graph_ref, GraphRef};
13+
14+
#[derive(Debug, Serialize, StructOpt)]
15+
pub struct Check {
16+
/// <NAME>@<VARIANT> of graph in Apollo Studio to validate.
17+
/// @<VARIANT> may be left off, defaulting to @current
18+
#[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))]
19+
#[serde(skip_serializing)]
20+
graph: GraphRef,
21+
22+
/// Name of configuration profile to use
23+
#[structopt(long = "profile", default_value = "default")]
24+
#[serde(skip_serializing)]
25+
profile_name: String,
26+
27+
/// Path of .graphql/.gql schema file to push
28+
#[structopt(long = "schema", short = "s")]
29+
#[serde(skip_serializing)]
30+
schema_path: PathBuf,
31+
}
32+
33+
impl Check {
34+
pub fn run(&self) -> Result<RoverStdout> {
35+
let client =
36+
get_studio_client(&self.profile_name).context("Failed to get studio client")?;
37+
let schema = std::fs::read_to_string(&self.schema_path)
38+
.with_context(|| format!("Could not read file `{}`", &self.schema_path.display()))?;
39+
let res = check::run(
40+
check::check_schema_query::Variables {
41+
graph_id: self.graph.name.clone(),
42+
variant: Some(self.graph.variant.clone()),
43+
schema: Some(schema),
44+
},
45+
&client,
46+
)
47+
.context("Failed to validate schema")?;
48+
49+
tracing::info!(
50+
"Validated schema against metrics from variant {} on graph {}",
51+
&self.graph.variant,
52+
&self.graph.name
53+
);
54+
tracing::info!(
55+
"Compared {} schema changes against {} operations",
56+
res.changes.len(),
57+
res.number_of_checked_operations
58+
);
59+
60+
if let Some(url) = res.target_url {
61+
tracing::info!("View full details here");
62+
tracing::info!("{}", url.to_string());
63+
}
64+
65+
let num_failures = print_changes(&res.changes);
66+
67+
match num_failures {
68+
0 => Ok(RoverStdout::None),
69+
1 => Err(anyhow::anyhow!("Encountered 1 failure.")),
70+
_ => Err(anyhow::anyhow!("Encountered {} failures.", num_failures)),
71+
}
72+
}
73+
}
74+
75+
fn print_changes(
76+
checks: &[check::check_schema_query::CheckSchemaQueryServiceCheckSchemaDiffToPreviousChanges],
77+
) -> u64 {
78+
let mut num_failures = 0;
79+
80+
if !checks.is_empty() {
81+
let mut table = Table::new();
82+
table.add_row(row!["Change", "Code", "Description"]);
83+
for check in checks {
84+
let change = match check.severity {
85+
check::check_schema_query::ChangeSeverity::NOTICE => "PASS",
86+
check::check_schema_query::ChangeSeverity::FAILURE => {
87+
num_failures += 1;
88+
"FAIL"
89+
}
90+
_ => unreachable!("Unknown change severity"),
91+
};
92+
table.add_row(row![change, check.code, check.description]);
93+
}
94+
95+
eprintln!("{}", table);
96+
}
97+
98+
num_failures
99+
}

src/command/graph/mod.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod check;
12
mod fetch;
23
mod push;
34

@@ -20,13 +21,17 @@ pub enum Command {
2021

2122
/// Push a schema to Apollo Studio from a local file
2223
Push(push::Push),
24+
25+
/// ✔️ Validate changes to a graph
26+
Check(check::Check),
2327
}
2428

25-
impl<'a> Graph {
29+
impl Graph {
2630
pub fn run(&self) -> Result<RoverStdout> {
2731
match &self.command {
2832
Command::Fetch(command) => command.run(),
2933
Command::Push(command) => command.run(),
34+
Command::Check(command) => command.run(),
3035
}
3136
}
3237
}

0 commit comments

Comments
 (0)