Skip to content

Commit

Permalink
feat: structured output
Browse files Browse the repository at this point in the history
This commit adds a global `--json` flag that structures the output of Rover like so:

**success:**

{
  "data": {
    "sdl": {
      "contents": "type Person {\n  id: ID!\n  name: String\n  appearedIn: [Film]\n  directed: [Film]\n}\n\ntype Film {\n  id: ID!\n  title: String\n  actors: [Person]\n  director: Person\n}\n\ntype Query {\n  person(id: ID!): Person\n  people: [Person]\n  film(id: ID!): Film!\n  films: [Film]\n}\n",
      "type": "graph"
    }
  },
  "error": null
}

**errors:**

{
  "data": null,
  "error": {
    "message": "Could not find subgraph \"products\".",
    "suggestion": "Try running this command with one of the following valid subgraphs: [people, films]",
    "code": "E009"
  }
}
  • Loading branch information
EverlastingBugstopper committed Jul 15, 2021
1 parent 363110e commit 2c91188
Show file tree
Hide file tree
Showing 47 changed files with 260 additions and 162 deletions.
46 changes: 32 additions & 14 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ A minimal command in Rover would be laid out exactly like this:
pub struct MyNewCommand { }

impl MyNewCommand {
pub fn run(&self) -> Result<RoverStdout> {
Ok(RoverStdout::None)
pub fn run(&self) -> Result<RoverOutput> {
Ok(RoverOutput::None)
}
}
```
Expand All @@ -128,16 +128,16 @@ For our `graph hello` command, we'll add a new `hello.rs` file under `src/comman
use serde::Serialize;
use structopt::StructOpt;

use crate::command::RoverStdout;
use crate::command::RoverOutput;
use crate::Result;

#[derive(Debug, Serialize, StructOpt)]
pub struct Hello { }

impl Hello {
pub fn run(&self) -> Result<RoverStdout> {
pub fn run(&self) -> Result<RoverOutput> {
eprintln!("Hello, world!");
Ok(RoverStdout::None)
Ok(RoverOutput::None)
}
}
```
Expand Down Expand Up @@ -348,7 +348,7 @@ Before we go any further, lets make sure everything is set up properly. We're go
It should look something like this (you should make sure you are following the style of other commands when creating new ones):

```rust
pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverStdout> {
pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverOutput> {
let client = client_config.get_client(&self.profile_name)?;
let graph_ref = self.graph.to_string();
eprintln!(
Expand All @@ -362,7 +362,10 @@ pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverStdout> {
},
&client,
)?;
Ok(RoverStdout::PlainText(deleted_at))
println!("{:?}", deleted_at);

// TODO: Add a new output type!
Ok(RoverOutput::None)
}
```

Expand Down Expand Up @@ -399,17 +402,32 @@ Unfortunately this is not the cleanest API and doesn't match the pattern set by

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`
##### `RoverOutput`

Now that you can actually execute the `hello::run` query and return its result, you should create a new variant of `RoverStdout` in `src/command/output.rs` that is not `PlainText`. Your new variant should print the descriptor using the `print_descriptor` function, and print the raw content using `print_content`.
Now that you can actually execute the `hello::run` query and return its result, you should create a new variant of `RoverOutput` in `src/command/output.rs` that is not `None`. Your new variant should print the descriptor using the `print_descriptor` function, and print the raw content using `print_content`.

To do so, change the line `Ok(RoverStdout::PlainText(deleted_at))` to `Ok(RoverStdout::DeletedAt(deleted_at))`, add a new `DeletedAt(String)` variant to `RoverStdout`, and then match on it in `pub fn print(&self)`:
To do so, change the line `Ok(RoverOutput::None)` to `Ok(RoverOutput::DeletedAt(deleted_at))`, add a new `DeletedAt(String)` variant to `RoverOutput`, and then match on it in `pub fn print(&self)` and `pub fn get_json(&self)`:

```rust
...
RoverStdout::DeletedAt(timestamp) => {
print_descriptor("Deleted At");
print_content(&timestamp);
pub fn print(&self) {
match self {
...
RoverOutput::DeletedAt(timestamp) => {
print_descriptor("Deleted At");
print_content(&timestamp);
}
...
}
}

pub fn get_json(&self) -> Value {
match self {
...
RoverOutput::DeletedAt(timestamp) => {
json!({ "deleted_at": timestamp.to_string() })
}
...
}
}
```

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/rover-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ houston = {path = "../houston"}

# crates.io deps
camino = "1"
chrono = "0.4"
chrono = { version = "0.4", features = ["serde"] }
git-url-parse = "0.3.1"
git2 = { version = "0.13.20", default-features = false, features = ["vendored-openssl"] }
graphql_client = "0.9"
Expand Down
2 changes: 1 addition & 1 deletion crates/rover-client/src/operations/subgraph/list/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ mod runner;
mod types;

pub use runner::run;
pub use types::{SubgraphListInput, SubgraphListResponse};
pub use types::{SubgraphInfo, SubgraphListInput, SubgraphListResponse, SubgraphUpdatedAt};
7 changes: 5 additions & 2 deletions crates/rover-client/src/operations/subgraph/list/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,17 @@ fn format_subgraphs(subgraphs: &[QuerySubgraphInfo]) -> Vec<SubgraphInfo> {
.map(|subgraph| SubgraphInfo {
name: subgraph.name.clone(),
url: subgraph.url.clone(),
updated_at: subgraph.updated_at.clone().parse().ok(),
updated_at: SubgraphUpdatedAt {
local: subgraph.updated_at.clone().parse().ok(),
utc: subgraph.updated_at.clone().parse().ok(),
},
})
.collect();

// sort and reverse, so newer items come first. We use _unstable here, since
// we don't care which order equal items come in the list (it's unlikely that
// we'll even have equal items after all)
subgraphs.sort_unstable_by(|a, b| a.updated_at.cmp(&b.updated_at).reverse());
subgraphs.sort_unstable_by(|a, b| a.updated_at.utc.cmp(&b.updated_at.utc).reverse());

subgraphs
}
Expand Down
15 changes: 11 additions & 4 deletions crates/rover-client/src/operations/subgraph/list/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ pub(crate) type QueryGraphType = subgraph_list_query::SubgraphListQueryServiceIm

type QueryVariables = subgraph_list_query::Variables;

use chrono::{DateTime, Local};
use chrono::{DateTime, Local, Utc};
use serde::Serialize;

#[derive(Clone, PartialEq, Debug)]
pub struct SubgraphListInput {
Expand All @@ -22,16 +23,22 @@ impl From<SubgraphListInput> for QueryVariables {
}
}

#[derive(Clone, PartialEq, Debug)]
#[derive(Clone, Serialize, PartialEq, Debug)]
pub struct SubgraphListResponse {
pub subgraphs: Vec<SubgraphInfo>,
pub root_url: String,
pub graph_ref: GraphRef,
}

#[derive(Clone, PartialEq, Debug)]
#[derive(Clone, Serialize, PartialEq, Debug)]
pub struct SubgraphInfo {
pub name: String,
pub url: Option<String>, // optional, and may not be a real url
pub updated_at: Option<DateTime<Local>>,
pub updated_at: SubgraphUpdatedAt,
}

#[derive(Clone, Serialize, PartialEq, Debug)]
pub struct SubgraphUpdatedAt {
pub local: Option<DateTime<Local>>,
pub utc: Option<DateTime<Utc>>,
}
6 changes: 3 additions & 3 deletions crates/rover-client/src/shared/check_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serde::Serialize;

/// CheckResponse is the return type of the
/// `graph` and `subgraph` check operations
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Serialize, Clone, PartialEq)]
pub struct CheckResponse {
pub target_url: Option<String>,
pub number_of_checked_operations: i64,
Expand Down Expand Up @@ -58,7 +58,7 @@ impl CheckResponse {

/// ChangeSeverity indicates whether a proposed change
/// in a GraphQL schema passed or failed the check
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Serialize, Clone, PartialEq)]
pub enum ChangeSeverity {
/// The proposed schema has passed the checks
PASS,
Expand Down Expand Up @@ -89,7 +89,7 @@ impl fmt::Display for ChangeSeverity {
}
}

#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Serialize, Clone, PartialEq)]
pub struct SchemaChange {
/// The code associated with a given change
/// e.g. 'TYPE_REMOVED'
Expand Down
9 changes: 6 additions & 3 deletions crates/rover-client/src/shared/fetch_response.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
#[derive(Debug, Clone, PartialEq)]
use serde::Serialize;

#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct FetchResponse {
pub sdl: Sdl,
}

#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Sdl {
pub contents: String,
pub r#type: SdlType,
}

#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum SdlType {
Graph,
Subgraph,
Expand Down
3 changes: 2 additions & 1 deletion crates/rover-client/src/shared/graph_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use std::str::FromStr;
use crate::RoverClientError;

use regex::Regex;
use serde::Serialize;

#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Serialize, Clone, PartialEq)]
pub struct GraphRef {
pub name: String,
pub variant: String,
Expand Down
43 changes: 28 additions & 15 deletions src/bin/rover.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use command::RoverStdout;
use robot_panic::setup_panic;
use rover::*;
use rover::{cli::Rover, command::RoverOutput, Result};
use sputnik::Session;
use structopt::StructOpt;

use std::{process, thread};

use serde_json::json;

fn main() {
setup_panic!(Metadata {
name: PKG_NAME.into(),
Expand All @@ -14,22 +15,37 @@ fn main() {
homepage: PKG_HOMEPAGE.into(),
repository: PKG_REPOSITORY.into()
});
if let Err(error) = run() {
tracing::debug!(?error);
eprint!("{}", error);
process::exit(1)
} else {
process::exit(0)

let app = Rover::from_args();

match run(&app) {
Ok(output) => {
if app.json {
let data = output.get_internal_json();
println!("{}", json!({"data": data, "error": null}));
} else {
output.print();
}
process::exit(0)
}
Err(error) => {
if app.json {
println!("{}", json!({"data": null, "error": error}));
} else {
tracing::debug!(?error);
eprint!("{}", error);
}
process::exit(1)
}
}
}

fn run() -> Result<()> {
let app = cli::Rover::from_args();
fn run(app: &Rover) -> Result<RoverOutput> {
timber::init(app.log_level);
tracing::trace!(command_structure = ?app);

// attempt to create a new `Session` to capture anonymous usage data
let output: RoverStdout = match Session::new(&app) {
match Session::new(app) {
// if successful, report the usage data in the background
Ok(session) => {
// kicks off the reporting on a background thread
Expand Down Expand Up @@ -58,8 +74,5 @@ fn run() -> Result<()> {

// otherwise just run the app without reporting
Err(_) => app.run(),
}?;

output.print();
Ok(())
}
}
12 changes: 8 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ use reqwest::blocking::Client;
use serde::Serialize;
use structopt::{clap::AppSettings, StructOpt};

use crate::command::{self, RoverStdout};
use crate::command::{self, RoverOutput};
use crate::utils::{
client::StudioClientConfig,
env::{RoverEnv, RoverEnvKey},
stringify::from_display,
stringify::option_from_display,
version,
};
use crate::Result;
Expand Down Expand Up @@ -55,9 +55,13 @@ pub struct Rover {

/// Specify Rover's log level
#[structopt(long = "log", short = "l", global = true, possible_values = &LEVELS, case_insensitive = true)]
#[serde(serialize_with = "from_display")]
#[serde(serialize_with = "option_from_display")]
pub log_level: Option<Level>,

/// Use json output
#[structopt(long = "json", global = true)]
pub json: bool,

#[structopt(skip)]
#[serde(skip_serializing)]
pub env_store: RoverEnv,
Expand Down Expand Up @@ -147,7 +151,7 @@ pub enum Command {
}

impl Rover {
pub fn run(&self) -> Result<RoverStdout> {
pub fn run(&self) -> Result<RoverOutput> {
// before running any commands, we check if rover is up to date
// this only happens once a day automatically
// we skip this check for the `rover update` commands, since they
Expand Down
6 changes: 3 additions & 3 deletions src/command/config/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use structopt::StructOpt;
use config::Profile;
use houston as config;

use crate::command::RoverStdout;
use crate::command::RoverOutput;
use crate::{anyhow, Result};

#[derive(Debug, Serialize, StructOpt)]
Expand All @@ -26,13 +26,13 @@ pub struct Auth {
}

impl Auth {
pub fn run(&self, config: config::Config) -> Result<RoverStdout> {
pub fn run(&self, config: config::Config) -> Result<RoverOutput> {
let api_key = api_key_prompt()?;
Profile::set_api_key(&self.profile_name, &config, &api_key)?;
Profile::get_credential(&self.profile_name, &config).map(|_| {
eprintln!("Successfully saved API key.");
})?;
Ok(RoverStdout::None)
Ok(RoverOutput::None)
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/command/config/clear.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use serde::Serialize;
use structopt::StructOpt;

use crate::command::RoverStdout;
use crate::command::RoverOutput;
use crate::Result;

use houston as config;
Expand All @@ -13,9 +13,9 @@ use houston as config;
pub struct Clear {}

impl Clear {
pub fn run(&self, config: config::Config) -> Result<RoverStdout> {
pub fn run(&self, config: config::Config) -> Result<RoverOutput> {
config.clear()?;
eprintln!("Successfully cleared all configuration.");
Ok(RoverStdout::None)
Ok(RoverOutput::None)
}
}
Loading

0 comments on commit 2c91188

Please sign in to comment.