Skip to content

Commit e96872e

Browse files
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" } }
1 parent a16f4f7 commit e96872e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+260
-162
lines changed

ARCHITECTURE.md

+32-14
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ A minimal command in Rover would be laid out exactly like this:
116116
pub struct MyNewCommand { }
117117

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

131-
use crate::command::RoverStdout;
131+
use crate::command::RoverOutput;
132132
use crate::Result;
133133

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

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

350350
```rust
351-
pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverStdout> {
351+
pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverOutput> {
352352
let client = client_config.get_client(&self.profile_name)?;
353353
let graph_ref = self.graph.to_string();
354354
eprintln!(
@@ -362,7 +362,10 @@ pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverStdout> {
362362
},
363363
&client,
364364
)?;
365-
Ok(RoverStdout::PlainText(deleted_at))
365+
println!("{:?}", deleted_at);
366+
367+
// TODO: Add a new output type!
368+
Ok(RoverOutput::None)
366369
}
367370
```
368371

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

400403
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.
401404

402-
##### `RoverStdout`
405+
##### `RoverOutput`
403406

404-
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`.
407+
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`.
405408

406-
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)`:
409+
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)`:
407410

408411
```rust
409-
...
410-
RoverStdout::DeletedAt(timestamp) => {
411-
print_descriptor("Deleted At");
412-
print_content(&timestamp);
412+
pub fn print(&self) {
413+
match self {
414+
...
415+
RoverOutput::DeletedAt(timestamp) => {
416+
print_descriptor("Deleted At");
417+
print_content(&timestamp);
418+
}
419+
...
420+
}
421+
}
422+
423+
pub fn get_json(&self) -> Value {
424+
match self {
425+
...
426+
RoverOutput::DeletedAt(timestamp) => {
427+
json!({ "deleted_at": timestamp.to_string() })
428+
}
429+
...
430+
}
413431
}
414432
```
415433

Cargo.lock

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

crates/rover-client/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ houston = {path = "../houston"}
1212

1313
# crates.io deps
1414
camino = "1"
15-
chrono = "0.4"
15+
chrono = { version = "0.4", features = ["serde"] }
1616
git-url-parse = "0.3.1"
1717
git2 = { version = "0.13.20", default-features = false, features = ["vendored-openssl"] }
1818
graphql_client = "0.9"

crates/rover-client/src/operations/subgraph/list/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ mod runner;
22
mod types;
33

44
pub use runner::run;
5-
pub use types::{SubgraphListInput, SubgraphListResponse};
5+
pub use types::{SubgraphInfo, SubgraphListInput, SubgraphListResponse, SubgraphUpdatedAt};

crates/rover-client/src/operations/subgraph/list/runner.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,17 @@ fn format_subgraphs(subgraphs: &[QuerySubgraphInfo]) -> Vec<SubgraphInfo> {
7878
.map(|subgraph| SubgraphInfo {
7979
name: subgraph.name.clone(),
8080
url: subgraph.url.clone(),
81-
updated_at: subgraph.updated_at.clone().parse().ok(),
81+
updated_at: SubgraphUpdatedAt {
82+
local: subgraph.updated_at.clone().parse().ok(),
83+
utc: subgraph.updated_at.clone().parse().ok(),
84+
},
8285
})
8386
.collect();
8487

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

9093
subgraphs
9194
}

crates/rover-client/src/operations/subgraph/list/types.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ pub(crate) type QueryGraphType = subgraph_list_query::SubgraphListQueryServiceIm
66

77
type QueryVariables = subgraph_list_query::Variables;
88

9-
use chrono::{DateTime, Local};
9+
use chrono::{DateTime, Local, Utc};
10+
use serde::Serialize;
1011

1112
#[derive(Clone, PartialEq, Debug)]
1213
pub struct SubgraphListInput {
@@ -22,16 +23,22 @@ impl From<SubgraphListInput> for QueryVariables {
2223
}
2324
}
2425

25-
#[derive(Clone, PartialEq, Debug)]
26+
#[derive(Clone, Serialize, PartialEq, Debug)]
2627
pub struct SubgraphListResponse {
2728
pub subgraphs: Vec<SubgraphInfo>,
2829
pub root_url: String,
2930
pub graph_ref: GraphRef,
3031
}
3132

32-
#[derive(Clone, PartialEq, Debug)]
33+
#[derive(Clone, Serialize, PartialEq, Debug)]
3334
pub struct SubgraphInfo {
3435
pub name: String,
3536
pub url: Option<String>, // optional, and may not be a real url
36-
pub updated_at: Option<DateTime<Local>>,
37+
pub updated_at: SubgraphUpdatedAt,
38+
}
39+
40+
#[derive(Clone, Serialize, PartialEq, Debug)]
41+
pub struct SubgraphUpdatedAt {
42+
pub local: Option<DateTime<Local>>,
43+
pub utc: Option<DateTime<Utc>>,
3744
}

crates/rover-client/src/shared/check_response.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use serde::Serialize;
99

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

5959
/// ChangeSeverity indicates whether a proposed change
6060
/// in a GraphQL schema passed or failed the check
61-
#[derive(Debug, Clone, PartialEq)]
61+
#[derive(Debug, Serialize, Clone, PartialEq)]
6262
pub enum ChangeSeverity {
6363
/// The proposed schema has passed the checks
6464
PASS,
@@ -89,7 +89,7 @@ impl fmt::Display for ChangeSeverity {
8989
}
9090
}
9191

92-
#[derive(Debug, Clone, PartialEq)]
92+
#[derive(Debug, Serialize, Clone, PartialEq)]
9393
pub struct SchemaChange {
9494
/// The code associated with a given change
9595
/// e.g. 'TYPE_REMOVED'

crates/rover-client/src/shared/fetch_response.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
#[derive(Debug, Clone, PartialEq)]
1+
use serde::Serialize;
2+
3+
#[derive(Debug, Clone, Serialize, PartialEq)]
24
pub struct FetchResponse {
35
pub sdl: Sdl,
46
}
57

6-
#[derive(Debug, Clone, PartialEq)]
8+
#[derive(Debug, Clone, Serialize, PartialEq)]
79
pub struct Sdl {
810
pub contents: String,
911
pub r#type: SdlType,
1012
}
1113

12-
#[derive(Debug, Clone, PartialEq)]
14+
#[derive(Debug, Clone, Serialize, PartialEq)]
15+
#[serde(rename_all(serialize = "lowercase"))]
1316
pub enum SdlType {
1417
Graph,
1518
Subgraph,

crates/rover-client/src/shared/graph_ref.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use std::str::FromStr;
44
use crate::RoverClientError;
55

66
use regex::Regex;
7+
use serde::Serialize;
78

8-
#[derive(Debug, Clone, PartialEq)]
9+
#[derive(Debug, Serialize, Clone, PartialEq)]
910
pub struct GraphRef {
1011
pub name: String,
1112
pub variant: String,

src/bin/rover.rs

+28-15
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
use command::RoverStdout;
21
use robot_panic::setup_panic;
3-
use rover::*;
2+
use rover::{cli::Rover, command::RoverOutput, Result};
43
use sputnik::Session;
54
use structopt::StructOpt;
65

76
use std::{process, thread};
87

8+
use serde_json::json;
9+
910
fn main() {
1011
setup_panic!(Metadata {
1112
name: PKG_NAME.into(),
@@ -14,22 +15,37 @@ fn main() {
1415
homepage: PKG_HOMEPAGE.into(),
1516
repository: PKG_REPOSITORY.into()
1617
});
17-
if let Err(error) = run() {
18-
tracing::debug!(?error);
19-
eprint!("{}", error);
20-
process::exit(1)
21-
} else {
22-
process::exit(0)
18+
19+
let app = Rover::from_args();
20+
21+
match run(&app) {
22+
Ok(output) => {
23+
if app.json {
24+
let data = output.get_internal_json();
25+
println!("{}", json!({"data": data, "error": null}));
26+
} else {
27+
output.print();
28+
}
29+
process::exit(0)
30+
}
31+
Err(error) => {
32+
if app.json {
33+
println!("{}", json!({"data": null, "error": error}));
34+
} else {
35+
tracing::debug!(?error);
36+
eprint!("{}", error);
37+
}
38+
process::exit(1)
39+
}
2340
}
2441
}
2542

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

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

5975
// otherwise just run the app without reporting
6076
Err(_) => app.run(),
61-
}?;
62-
63-
output.print();
64-
Ok(())
77+
}
6578
}

src/cli.rs

+8-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ use reqwest::blocking::Client;
22
use serde::Serialize;
33
use structopt::{clap::AppSettings, StructOpt};
44

5-
use crate::command::{self, RoverStdout};
5+
use crate::command::{self, RoverOutput};
66
use crate::utils::{
77
client::StudioClientConfig,
88
env::{RoverEnv, RoverEnvKey},
9-
stringify::from_display,
9+
stringify::option_from_display,
1010
version,
1111
};
1212
use crate::Result;
@@ -55,9 +55,13 @@ pub struct Rover {
5555

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

61+
/// Use json output
62+
#[structopt(long = "json", global = true)]
63+
pub json: bool,
64+
6165
#[structopt(skip)]
6266
#[serde(skip_serializing)]
6367
pub env_store: RoverEnv,
@@ -147,7 +151,7 @@ pub enum Command {
147151
}
148152

149153
impl Rover {
150-
pub fn run(&self) -> Result<RoverStdout> {
154+
pub fn run(&self) -> Result<RoverOutput> {
151155
// before running any commands, we check if rover is up to date
152156
// this only happens once a day automatically
153157
// we skip this check for the `rover update` commands, since they

src/command/config/auth.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use structopt::StructOpt;
55
use config::Profile;
66
use houston as config;
77

8-
use crate::command::RoverStdout;
8+
use crate::command::RoverOutput;
99
use crate::{anyhow, Result};
1010

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

2828
impl Auth {
29-
pub fn run(&self, config: config::Config) -> Result<RoverStdout> {
29+
pub fn run(&self, config: config::Config) -> Result<RoverOutput> {
3030
let api_key = api_key_prompt()?;
3131
Profile::set_api_key(&self.profile_name, &config, &api_key)?;
3232
Profile::get_credential(&self.profile_name, &config).map(|_| {
3333
eprintln!("Successfully saved API key.");
3434
})?;
35-
Ok(RoverStdout::None)
35+
Ok(RoverOutput::None)
3636
}
3737
}
3838

src/command/config/clear.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use serde::Serialize;
22
use structopt::StructOpt;
33

4-
use crate::command::RoverStdout;
4+
use crate::command::RoverOutput;
55
use crate::Result;
66

77
use houston as config;
@@ -13,9 +13,9 @@ use houston as config;
1313
pub struct Clear {}
1414

1515
impl Clear {
16-
pub fn run(&self, config: config::Config) -> Result<RoverStdout> {
16+
pub fn run(&self, config: config::Config) -> Result<RoverOutput> {
1717
config.clear()?;
1818
eprintln!("Successfully cleared all configuration.");
19-
Ok(RoverStdout::None)
19+
Ok(RoverOutput::None)
2020
}
2121
}

0 commit comments

Comments
 (0)