Skip to content

Commit

Permalink
chore: begin adding shared types and consolidate check operations (#652)
Browse files Browse the repository at this point in the history
  • Loading branch information
EverlastingBugstopper authored Jul 6, 2021
1 parent 9e83b88 commit e29b87b
Show file tree
Hide file tree
Showing 86 changed files with 531 additions and 376 deletions.
30 changes: 19 additions & 11 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,13 @@ For more information try --help

##### Setting up a command to work with `rover-client`

Most of Rover's commands make requests to Apollo Studio's API. Rather than handling the request logic in the repository's main package, Rover is structured so that this logic lives in `crates/rover-client`. This is helpful for separation of concerns and testing.
Most of Rover's commands make requests to Apollo Studio's API, or to another GraphQL API. Rather than handling the request logic in the repository's main package, Rover is structured so that this logic lives in `crates/rover-client`. This is helpful for separation of concerns and testing.

To access functionality from `rover-client` in our `rover graph hello` command, we'll need to pass down a client from the entry to our command in `src/command/graph/mod.rs`.

You can do this by changing the `Command::Hello(command) => command.run(),` line to `Command::Hello(command) => command.run(client_config),`.

Then you'll need to change `Hello::run` to accept a `client_config: StudioClientConfig` parameter in `src/command/graph/hello.rs`, and add a `use crate::utils::client::StudioClientConfig` import statement. Then, at the top of the run function, you can create a `StudioClient` by adding `let client = client_config.get_client(&self.profile_name)?;`. You can see examples of this in the other commands.
Then you'll need to change `Hello::run` to accept a `client_config: StudioClientConfig` parameter in `src/command/graph/hello.rs`, and add a `use crate::utils::client::StudioClientConfig` import statement. Then, at the top of the run function, you can create a `StudioClient` by adding `let client = client_config.get_authenticated_client(&self.profile_name)?;`. You can see examples of this in the other commands.

##### Auto-generated help command

Expand Down Expand Up @@ -271,15 +271,15 @@ Whenever you create a new command, make sure to add `#[serde(skip_serializing)]`

##### Adding a query to Apollo Studio

The only piece of the `rover-client` crate that we need to be concerned with for now is the `src/query` directory. This is where all the queries to Apollo Studio live. This directory is roughly organized by the command names as well, but there might be some queries in these directories that are used by multiple commands.
The only piece of the `rover-client` crate that we need to be concerned with for now is the `src/operations` directory. This is where all the queries to Apollo Studio live. This directory is roughly organized by the command names as well, but there might be some queries in these directories that are used by multiple commands.

You can see in the `src/query/graph` directory a number of `.rs` files paired with `.graphql` files. The `.graphql` files are the files where the GraphQL operations live, and the matching `.rs` files contain the logic needed to execute those operations.
You can see in the `src/operations/graph` directory a number of `.rs` files paired with `.graphql` files. The `.graphql` files are the files where the GraphQL operations live, and the matching `.rs` files contain the logic needed to execute those operations.

##### Writing a GraphQL operation

For our basic `graph hello` command, we're going to make a request to Apollo Studio that inquires about the existence of a particular graph, and nothing else. For this, we can use the `Query.service` field.

Create a `hello.graphql` file in `crates/rover-client/src/query/graph` and paste the following into it:
Create a `hello.graphql` file in `crates/rover-client/src/operations/graph` and paste the following into it:

```graphql
query GraphHello($graphId: ID!) {
Expand All @@ -295,17 +295,19 @@ This basic GraphQL operation uses a graph's unique ID (which we get from the `Gr

This project uses [graphql-client](https://docs.rs/graphql_client/latest/graphql_client/) to generate types for each raw `.graphql` query that we write.

First, create an empty file at `crates/rover-client/src/query/graph/hello.rs`.
First, create an empty directory at `crates/rover-client/src/operations/graph/hello`, and then in that directory, create a `mod.rs` file to initialize the module.

To start compiling this file, we need to export the module in `crates/rover-client/src/query/graph/mod.rs`:
To start compiling this file, we need to export the module in `crates/rover-client/src/operations/graph/mod.rs`:

```rust
...
/// "Graph hello" command execution
/// "graph hello" command execution
pub mod hello;
```

Back in `hello.rs`, we'll import the following types:
Back in our `hello` module, we'll create a `runner.rs`, and add `mod runner` to our `mod.rs` file.

Then, in `runner.rs`, import the following types:

```rust
use crate::blocking::StudioClient;
Expand All @@ -320,7 +322,7 @@ Then, we'll create a new struct that will have auto-generated types for the `hel
// The paths are relative to the directory where your `Cargo.toml` is located.
// Both json and the GraphQL schema language are supported as sources for the schema
#[graphql(
query_path = "src/query/graph/hello.graphql",
query_path = "src/operations/graph/hello/hello_query.graphql",
schema_path = ".schema/schema.graphql",
response_derives = "PartialEq, Debug, Serialize, Deserialize",
deprecated = "warn"
Expand Down Expand Up @@ -395,7 +397,13 @@ fn build_response(
}
```

This should get you to the point where you can run `rover graph hello <GRAPH_REF>` and see if and when the last graph was deleted. From here, you should be able to follow the examples of other commands to write out tests for the `build_response` function. This is left as an exercise for the reader.
This should get you to the point where you can run `rover graph hello <GRAPH_REF>` and see if and when the last graph was deleted. From here, you should be able to follow the examples of other commands to write out tests for the `build_response` function.

##### Clean up the API

Unfortunately this is not the cleanest API and doesn't match the pattern set by the rest of the commands. Each `rover-client` operation has an input type and an output type, along with a `run` function that takes in a `reqwest::blocking::Client`.

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`

Expand Down
19 changes: 18 additions & 1 deletion crates/rover-client/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use reqwest::Url;
use thiserror::Error;

use crate::query::subgraph::check::types::CompositionError;
use crate::{operations::subgraph::check::types::CompositionError, shared::CheckResponse};

/// RoverClientError represents all possible failures that can occur during a client request.
#[derive(Error, Debug)]
Expand Down Expand Up @@ -132,6 +132,12 @@ pub enum RoverClientError {
#[error("Invalid ChangeSeverity.")]
InvalidSeverity,

/// 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))]
OperationCheckFailure { check_response: CheckResponse },

/// This error occurs when a user has a malformed API key
#[error(
"The API key you provided is malformed. An API key must have three parts separated by a colon."
Expand Down Expand Up @@ -169,3 +175,14 @@ fn subgraph_composition_error_msg(composition_errors: &[CompositionError]) -> St
});
msg
}

fn check_response_error_msg(check_response: &CheckResponse) -> String {
let plural = match check_response.num_failures {
1 => "",
_ => "s",
};
format!(
"This operation has encountered {} change{} that would break existing clients.",
check_response.num_failures, plural
)
}
4 changes: 2 additions & 2 deletions crates/rover-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ pub use error::RoverClientError;

#[allow(clippy::upper_case_acronyms)]
/// Module for actually querying studio
pub mod query;
pub mod operations;

/// Module for getting release info
pub mod releases;

/// Module for shared functionality
pub mod utils;
pub mod shared;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use graphql_client::*;
// The paths are relative to the directory where your `Cargo.toml` is located.
// Both json and the GraphQL schema language are supported as sources for the schema
#[graphql(
query_path = "src/query/config/is_federated.graphql",
query_path = "src/operations/config/is_federated.graphql",
schema_path = ".schema/schema.graphql",
response_derives = "PartialEq, Debug, Serialize, Deserialize",
deprecated = "warn"
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod types;

pub mod query_runner;
pub mod runner;
pub use types::{Actor, ConfigWhoAmIInput, RegistryIdentity};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::blocking::StudioClient;
use crate::query::config::who_am_i::{
use crate::operations::config::who_am_i::{
types::{QueryActorType, QueryResponseData, RegistryIdentity},
Actor, ConfigWhoAmIInput,
};
Expand All @@ -13,7 +13,7 @@ use graphql_client::*;
// The paths are relative to the directory where your `Cargo.toml` is located.
// Both json and the GraphQL schema language are supported as sources for the schema
#[graphql(
query_path = "src/query/config/who_am_i/who_am_i_query.graphql",
query_path = "src/operations/config/who_am_i/who_am_i_query.graphql",
schema_path = ".schema/schema.graphql",
response_derives = "PartialEq, Debug, Serialize, Deserialize",
deprecated = "warn"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::query_runner::config_who_am_i_query;
use super::runner::config_who_am_i_query;

use houston::CredentialOrigin;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
mutation CheckSchemaQuery(
$graphId: ID!
mutation GraphCheckMutation(
$graph_id: ID!
$variant: String
$schema: String
$gitContext: GitContextInput!
$proposed_schema: String
$git_context: GitContextInput!
$config: HistoricQueryParameters!
) {
service(id: $graphId) {
Expand All @@ -24,4 +24,4 @@ mutation CheckSchemaQuery(
}
}
}
}
}
5 changes: 5 additions & 0 deletions crates/rover-client/src/operations/graph/check/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod runner;
mod types;

pub use runner::run;
pub use types::GraphCheckInput;
Original file line number Diff line number Diff line change
@@ -1,76 +1,67 @@
use crate::blocking::StudioClient;
use crate::operations::graph::check::types::{
GraphCheckInput, MutationChangeSeverity, MutationResponseData,
};
use crate::shared::CheckResponse;
use crate::RoverClientError;
use graphql_client::*;

use reqwest::Url;
use graphql_client::*;

type Timestamp = String;
#[derive(GraphQLQuery)]
// The paths are relative to the directory where your `Cargo.toml` is located.
// Both json and the GraphQL schema language are supported as sources for the schema
#[graphql(
query_path = "src/query/graph/check.graphql",
query_path = "src/operations/graph/check/check_mutation.graphql",
schema_path = ".schema/schema.graphql",
response_derives = "PartialEq, Debug, Serialize, Deserialize",
deprecated = "warn"
)]
/// This struct is used to generate the module containing `Variables` and
/// `ResponseData` structs.
/// Snake case of this name is the mod name. i.e. check_schema_query
pub struct CheckSchemaQuery;
/// Snake case of this name is the mod name. i.e. graph_check_mutation
pub(crate) struct GraphCheckMutation;

/// The main function to be used from this module.
/// This function takes a proposed schema and validates it against a published
/// schema.
pub fn run(
variables: check_schema_query::Variables,
input: GraphCheckInput,
client: &StudioClient,
) -> Result<CheckResponse, RoverClientError> {
let graph = variables.graph_id.clone();
let data = client.post::<CheckSchemaQuery>(variables)?;
let graph = input.graph_id.clone();
let data = client.post::<GraphCheckMutation>(input.into())?;
get_check_response_from_data(data, graph)
}

#[derive(Debug)]
pub struct CheckResponse {
pub target_url: Option<Url>,
pub number_of_checked_operations: i64,
pub change_severity: check_schema_query::ChangeSeverity,
pub changes: Vec<check_schema_query::CheckSchemaQueryServiceCheckSchemaDiffToPreviousChanges>,
}

fn get_check_response_from_data(
data: check_schema_query::ResponseData,
data: MutationResponseData,
graph: String,
) -> Result<CheckResponse, RoverClientError> {
let service = data.service.ok_or(RoverClientError::NoService { graph })?;
let target_url = get_url(service.check_schema.target_url);
let target_url = service.check_schema.target_url;

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 change_severity = diff_to_previous.severity;
let changes = diff_to_previous.changes;
let change_severity = 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());
}

Ok(CheckResponse {
let check_response = CheckResponse {
target_url,
number_of_checked_operations,
change_severity,
changes,
})
}
change_severity,
num_failures,
};

fn get_url(url: Option<String>) -> Option<Url> {
match url {
Some(url) => {
let url = Url::parse(&url);
match url {
Ok(url) => Some(url),
// if the API returns an invalid URL, don't put it in the response
Err(_) => None,
}
}
None => None,
}
check_response.check_for_failures()
}
97 changes: 97 additions & 0 deletions crates/rover-client/src/operations/graph/check/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use crate::operations::graph::check::runner::graph_check_mutation;
use crate::shared::{ChangeSeverity, CheckConfig, GitContext, SchemaChange};

#[derive(Debug, Clone, PartialEq)]
pub struct GraphCheckInput {
pub graph_id: String,
pub variant: String,
pub proposed_schema: String,
pub git_context: GitContext,
pub config: CheckConfig,
}

impl From<GraphCheckInput> for MutationVariables {
fn from(input: GraphCheckInput) -> Self {
Self {
graph_id: input.graph_id,
variant: Some(input.variant),
proposed_schema: Some(input.proposed_schema),
config: input.config.into(),
git_context: input.git_context.into(),
}
}
}

type MutationConfig = graph_check_mutation::HistoricQueryParameters;
impl From<CheckConfig> for MutationConfig {
fn from(input: CheckConfig) -> Self {
Self {
query_count_threshold: input.query_count_threshold,
query_count_threshold_percentage: input.query_count_threshold_percentage,
from: input.validation_period_from,
to: input.validation_period_to,
// we don't support configuring these, but we can't leave them out
excluded_clients: None,
ignored_operations: None,
included_variants: None,
}
}
}

type MutationVariables = graph_check_mutation::Variables;
pub(crate) type MutationResponseData = graph_check_mutation::ResponseData;

pub(crate) type MutationChangeSeverity = graph_check_mutation::ChangeSeverity;
impl From<MutationChangeSeverity> for ChangeSeverity {
fn from(severity: MutationChangeSeverity) -> Self {
match severity {
MutationChangeSeverity::NOTICE => ChangeSeverity::PASS,
MutationChangeSeverity::FAILURE => ChangeSeverity::FAIL,
_ => ChangeSeverity::unreachable(),
}
}
}

impl From<ChangeSeverity> for MutationChangeSeverity {
fn from(severity: ChangeSeverity) -> Self {
match severity {
ChangeSeverity::PASS => MutationChangeSeverity::NOTICE,
ChangeSeverity::FAIL => MutationChangeSeverity::FAILURE,
}
}
}

type MutationSchemaChange =
graph_check_mutation::GraphCheckMutationServiceCheckSchemaDiffToPreviousChanges;
impl From<SchemaChange> for MutationSchemaChange {
fn from(schema_change: SchemaChange) -> MutationSchemaChange {
MutationSchemaChange {
severity: schema_change.severity.into(),
code: schema_change.code,
description: schema_change.description,
}
}
}

impl From<MutationSchemaChange> for SchemaChange {
fn from(schema_change: MutationSchemaChange) -> SchemaChange {
SchemaChange {
severity: schema_change.severity.into(),
code: schema_change.code,
description: schema_change.description,
}
}
}

type MutationGitContextInput = graph_check_mutation::GitContextInput;
impl From<GitContext> for MutationGitContextInput {
fn from(git_context: GitContext) -> MutationGitContextInput {
MutationGitContextInput {
branch: git_context.branch,
commit: git_context.commit,
committer: git_context.author,
remote_url: git_context.remote_url,
message: None,
}
}
}
Loading

0 comments on commit e29b87b

Please sign in to comment.