Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: structured output #676

Merged
Merged
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ab64ba2
feat: structured output
EverlastingBugstopper Jul 15, 2021
61045a4
chore: fix main imports
EverlastingBugstopper Jul 15, 2021
80c6e43
chore: rename check response fields
EverlastingBugstopper Jul 16, 2021
b9c74bc
feat: structure composition errors (#678)
EverlastingBugstopper Jul 19, 2021
3f62257
feat: structure sub/graph publish (#679)
EverlastingBugstopper Jul 19, 2021
cc4dad2
chore: adds data.success bool to json output (#681)
EverlastingBugstopper Jul 19, 2021
92f9ce4
chore: test structured output
EverlastingBugstopper Jul 19, 2021
1e089b4
chore: structure subgraph delete
EverlastingBugstopper Jul 20, 2021
b998239
chore: rename schema_hash to api_schema_hash
EverlastingBugstopper Jul 21, 2021
5bf3550
chore: test serializer for composition errors
EverlastingBugstopper Jul 23, 2021
0b52c70
chore(json): move all composition errors to top level data
EverlastingBugstopper Jul 23, 2021
a2c5c12
chore(json): fix up operation check errors
EverlastingBugstopper Jul 23, 2021
4e66dba
chore(json): add from implementations to ChangeSummary
EverlastingBugstopper Jul 23, 2021
14e59ad
fixup: remove unnecessary match for operation checks
EverlastingBugstopper Jul 23, 2021
291c81a
fixup: add tests for operation check errors
EverlastingBugstopper Jul 23, 2021
5747172
chore(json): --json -> --output json
EverlastingBugstopper Jul 23, 2021
fdceb28
chore: address lints
EverlastingBugstopper Jul 26, 2021
ea99465
fixup: formatting
EverlastingBugstopper Jul 26, 2021
3dc6070
chore: error.composition_errors -> error.details.build_errors
EverlastingBugstopper Jul 26, 2021
4806738
chore: add top level version to json output
EverlastingBugstopper Jul 26, 2021
94b44cf
chore: finish cleaning up build error renaming
EverlastingBugstopper Jul 26, 2021
f38617d
chore: address clippy
EverlastingBugstopper Jul 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: structured output
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"
  }
}
EverlastingBugstopper committed Jul 22, 2021

Verified

This commit was signed with the committer’s verified signature.
lexnv Alexandru Vasile
commit ab64ba265ad89094d8c691b9a242f1a41590807d
46 changes: 32 additions & 14 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -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)
}
}
```
@@ -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)
}
}
```
@@ -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!(
@@ -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)
}
```

@@ -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() })
}
...
}
}
```

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
@@ -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"
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
@@ -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
@@ -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
}
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
@@ -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 {
@@ -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
@@ -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,
@@ -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,
@@ -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'
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,
3 changes: 2 additions & 1 deletion crates/rover-client/src/shared/graph_ref.rs
Original file line number Diff line number Diff line change
@@ -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,
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(),
@@ -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
@@ -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
@@ -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;
@@ -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,
@@ -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
6 changes: 3 additions & 3 deletions src/command/config/auth.rs
Original file line number Diff line number Diff line change
@@ -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)]
@@ -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)
}
}

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;
@@ -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