diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 599f97fa49..1b117879a3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 { - Ok(RoverStdout::None) + pub fn run(&self) -> Result { + 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 { + pub fn run(&self) -> Result { 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 { +pub fn run(&self, client_config: StudioClientConfig) -> Result { 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 { }, &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(×tamp); +pub fn print(&self) { + match self { + ... + RoverOutput::DeletedAt(timestamp) => { + print_descriptor("Deleted At"); + print_content(×tamp); + } + ... + } +} + +pub fn get_json(&self) -> Value { + match self { + ... + RoverOutput::DeletedAt(timestamp) => { + json!({ "deleted_at": timestamp.to_string() }) + } + ... + } } ``` diff --git a/Cargo.lock b/Cargo.lock index 8360b53401..f555c56fa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,6 +321,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi", ] diff --git a/crates/rover-client/Cargo.toml b/crates/rover-client/Cargo.toml index 99d56f702e..18ec320094 100644 --- a/crates/rover-client/Cargo.toml +++ b/crates/rover-client/Cargo.toml @@ -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" diff --git a/crates/rover-client/src/operations/subgraph/list/mod.rs b/crates/rover-client/src/operations/subgraph/list/mod.rs index 912e0a83c6..dd493dd4c8 100644 --- a/crates/rover-client/src/operations/subgraph/list/mod.rs +++ b/crates/rover-client/src/operations/subgraph/list/mod.rs @@ -2,4 +2,4 @@ mod runner; mod types; pub use runner::run; -pub use types::{SubgraphListInput, SubgraphListResponse}; +pub use types::{SubgraphInfo, SubgraphListInput, SubgraphListResponse, SubgraphUpdatedAt}; diff --git a/crates/rover-client/src/operations/subgraph/list/runner.rs b/crates/rover-client/src/operations/subgraph/list/runner.rs index 1a5a2c3f64..2b99127834 100644 --- a/crates/rover-client/src/operations/subgraph/list/runner.rs +++ b/crates/rover-client/src/operations/subgraph/list/runner.rs @@ -78,14 +78,17 @@ fn format_subgraphs(subgraphs: &[QuerySubgraphInfo]) -> Vec { .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 } diff --git a/crates/rover-client/src/operations/subgraph/list/types.rs b/crates/rover-client/src/operations/subgraph/list/types.rs index af260a33db..882051241f 100644 --- a/crates/rover-client/src/operations/subgraph/list/types.rs +++ b/crates/rover-client/src/operations/subgraph/list/types.rs @@ -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 for QueryVariables { } } -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Serialize, PartialEq, Debug)] pub struct SubgraphListResponse { pub subgraphs: Vec, 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, // optional, and may not be a real url - pub updated_at: Option>, + pub updated_at: SubgraphUpdatedAt, +} + +#[derive(Clone, Serialize, PartialEq, Debug)] +pub struct SubgraphUpdatedAt { + pub local: Option>, + pub utc: Option>, } diff --git a/crates/rover-client/src/shared/check_response.rs b/crates/rover-client/src/shared/check_response.rs index 135bda99c8..2622a1775e 100644 --- a/crates/rover-client/src/shared/check_response.rs +++ b/crates/rover-client/src/shared/check_response.rs @@ -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, 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' diff --git a/crates/rover-client/src/shared/fetch_response.rs b/crates/rover-client/src/shared/fetch_response.rs index 5f4ab4381f..0c2500caaf 100644 --- a/crates/rover-client/src/shared/fetch_response.rs +++ b/crates/rover-client/src/shared/fetch_response.rs @@ -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, diff --git a/crates/rover-client/src/shared/graph_ref.rs b/crates/rover-client/src/shared/graph_ref.rs index 5cad87c3e0..99d0c16f1c 100644 --- a/crates/rover-client/src/shared/graph_ref.rs +++ b/crates/rover-client/src/shared/graph_ref.rs @@ -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, diff --git a/src/bin/rover.rs b/src/bin/rover.rs index cb663447cd..1d02376e26 100644 --- a/src/bin/rover.rs +++ b/src/bin/rover.rs @@ -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 { 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(()) + } } diff --git a/src/cli.rs b/src/cli.rs index 67224b47c7..74efae380b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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, + /// 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 { + pub fn run(&self) -> Result { // 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 diff --git a/src/command/config/auth.rs b/src/command/config/auth.rs index ae15cb16f4..efaacda889 100644 --- a/src/command/config/auth.rs +++ b/src/command/config/auth.rs @@ -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 { + pub fn run(&self, config: config::Config) -> Result { 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) } } diff --git a/src/command/config/clear.rs b/src/command/config/clear.rs index a587885cb0..dfcda7b77b 100644 --- a/src/command/config/clear.rs +++ b/src/command/config/clear.rs @@ -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 { + pub fn run(&self, config: config::Config) -> Result { config.clear()?; eprintln!("Successfully cleared all configuration."); - Ok(RoverStdout::None) + Ok(RoverOutput::None) } } diff --git a/src/command/config/delete.rs b/src/command/config/delete.rs index 9c9664b4f1..0df08911a1 100644 --- a/src/command/config/delete.rs +++ b/src/command/config/delete.rs @@ -3,7 +3,7 @@ use structopt::StructOpt; use houston as config; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; #[derive(Debug, Serialize, StructOpt)] @@ -20,9 +20,9 @@ pub struct Delete { } impl Delete { - pub fn run(&self, config: config::Config) -> Result { + pub fn run(&self, config: config::Config) -> Result { config::Profile::delete(&self.name, &config)?; eprintln!("Successfully deleted profile \"{}\"", &self.name); - Ok(RoverStdout::None) + Ok(RoverOutput::None) } } diff --git a/src/command/config/list.rs b/src/command/config/list.rs index 907bc0f9ce..9bd56b892c 100644 --- a/src/command/config/list.rs +++ b/src/command/config/list.rs @@ -4,15 +4,15 @@ use structopt::StructOpt; use crate::Result; use houston as config; -use crate::command::RoverStdout; +use crate::command::RoverOutput; #[derive(Serialize, Debug, StructOpt)] /// List all configuration profiles pub struct List {} impl List { - pub fn run(&self, config: config::Config) -> Result { + pub fn run(&self, config: config::Config) -> Result { let profiles = config::Profile::list(&config)?; - Ok(RoverStdout::Profiles(profiles)) + Ok(RoverOutput::Profiles(profiles)) } } diff --git a/src/command/config/mod.rs b/src/command/config/mod.rs index a9eed151aa..71a1b87aa1 100644 --- a/src/command/config/mod.rs +++ b/src/command/config/mod.rs @@ -7,7 +7,7 @@ mod whoami; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -36,7 +36,7 @@ pub enum Command { } impl Config { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { match &self.command { Command::Auth(command) => command.run(client_config.config), Command::List(command) => command.run(client_config.config), diff --git a/src/command/config/whoami.rs b/src/command/config/whoami.rs index 7398f9cebf..a5dad81e84 100644 --- a/src/command/config/whoami.rs +++ b/src/command/config/whoami.rs @@ -6,7 +6,7 @@ use structopt::StructOpt; use houston::CredentialOrigin; use crate::anyhow; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::utils::env::RoverEnvKey; use crate::Result; @@ -22,7 +22,7 @@ pub struct WhoAmI { } impl WhoAmI { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; eprintln!("Checking identity of your API key against the registry."); @@ -80,6 +80,6 @@ impl WhoAmI { eprintln!("{}", message); - Ok(RoverStdout::None) + Ok(RoverOutput::None) } } diff --git a/src/command/docs/list.rs b/src/command/docs/list.rs index 6a1190f2bf..3f951f57ad 100644 --- a/src/command/docs/list.rs +++ b/src/command/docs/list.rs @@ -1,4 +1,4 @@ -use crate::{command::RoverStdout, Result}; +use crate::{command::RoverOutput, Result}; use super::shortlinks; @@ -9,8 +9,8 @@ use structopt::StructOpt; pub struct List {} impl List { - pub fn run(&self) -> Result { - Ok(RoverStdout::DocsList( + pub fn run(&self) -> Result { + Ok(RoverOutput::DocsList( shortlinks::get_shortlinks_with_description(), )) } diff --git a/src/command/docs/mod.rs b/src/command/docs/mod.rs index 0950c0eb4b..ce3c0f6c80 100644 --- a/src/command/docs/mod.rs +++ b/src/command/docs/mod.rs @@ -5,7 +5,7 @@ pub mod shortlinks; use serde::Serialize; use structopt::StructOpt; -use crate::{command::RoverStdout, Result}; +use crate::{command::RoverOutput, Result}; #[derive(Debug, Serialize, StructOpt)] pub struct Docs { @@ -23,7 +23,7 @@ pub enum Command { } impl Docs { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { match &self.command { Command::List(command) => command.run(), Command::Open(command) => command.run(), diff --git a/src/command/docs/open.rs b/src/command/docs/open.rs index 7e43ff6db3..f446fbca43 100644 --- a/src/command/docs/open.rs +++ b/src/command/docs/open.rs @@ -1,4 +1,4 @@ -use crate::{anyhow, command::RoverStdout, Result}; +use crate::{anyhow, command::RoverOutput, Result}; use super::shortlinks; @@ -15,7 +15,7 @@ pub struct Open { } impl Open { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { let url = shortlinks::get_url_from_slug(&self.slug); let yellow_browser_var = format!("{}", Yellow.normal().paint("$BROWSER")); let cyan_url = format!("{}", Cyan.normal().paint(&url)); @@ -40,6 +40,6 @@ impl Open { Ok(()) }?; - Ok(RoverStdout::None) + Ok(RoverOutput::None) } } diff --git a/src/command/explain.rs b/src/command/explain.rs index 4c2d3a5ad5..f09d9b6455 100644 --- a/src/command/explain.rs +++ b/src/command/explain.rs @@ -1,4 +1,4 @@ -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::error::metadata::code::Code; use crate::Result; use serde::Serialize; @@ -12,8 +12,8 @@ pub struct Explain { } impl Explain { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { let explanation = &self.code.explain(); - Ok(RoverStdout::Markdown(explanation.clone())) + Ok(RoverOutput::Markdown(explanation.clone())) } } diff --git a/src/command/graph/check.rs b/src/command/graph/check.rs index 97035155b5..d188510218 100644 --- a/src/command/graph/check.rs +++ b/src/command/graph/check.rs @@ -4,7 +4,7 @@ use structopt::StructOpt; use rover_client::operations::graph::check::{self, GraphCheckInput}; use rover_client::shared::{CheckConfig, GitContext, GraphRef, ValidationPeriod}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{ @@ -53,7 +53,7 @@ impl Check { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let proposed_schema = load_schema_from_flag(&self.schema, std::io::stdin())?; @@ -76,6 +76,6 @@ impl Check { &client, )?; - Ok(RoverStdout::CheckResponse(res)) + Ok(RoverOutput::CheckResponse(res)) } } diff --git a/src/command/graph/fetch.rs b/src/command/graph/fetch.rs index 2e01fc0f89..478811b2df 100644 --- a/src/command/graph/fetch.rs +++ b/src/command/graph/fetch.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use rover_client::operations::graph::fetch::{self, GraphFetchInput}; use rover_client::shared::GraphRef; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -24,7 +24,7 @@ pub struct Fetch { } impl Fetch { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -40,6 +40,6 @@ impl Fetch { &client, )?; - Ok(RoverStdout::FetchResponse(fetch_response)) + Ok(RoverOutput::FetchResponse(fetch_response)) } } diff --git a/src/command/graph/introspect.rs b/src/command/graph/introspect.rs index 991f09e5d1..da66e047b0 100644 --- a/src/command/graph/introspect.rs +++ b/src/command/graph/introspect.rs @@ -10,7 +10,7 @@ use rover_client::{ operations::graph::introspect::{self, GraphIntrospectInput}, }; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::parsers::parse_header; #[derive(Debug, Serialize, StructOpt)] @@ -31,7 +31,7 @@ pub struct Introspect { } impl Introspect { - pub fn run(&self, client: Client) -> Result { + pub fn run(&self, client: Client) -> Result { let client = GraphQLClient::new(&self.endpoint.to_string(), client)?; // add the flag headers to a hashmap to pass along to rover-client @@ -44,7 +44,7 @@ impl Introspect { let introspection_response = introspect::run(GraphIntrospectInput { headers }, &client)?; - Ok(RoverStdout::Introspection( + Ok(RoverOutput::Introspection( introspection_response.schema_sdl, )) } diff --git a/src/command/graph/mod.rs b/src/command/graph/mod.rs index 4106906124..0603fb4e5e 100644 --- a/src/command/graph/mod.rs +++ b/src/command/graph/mod.rs @@ -6,7 +6,7 @@ mod publish; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -39,7 +39,7 @@ impl Graph { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { match &self.command { Command::Check(command) => command.run(client_config, git_context), Command::Fetch(command) => command.run(client_config), diff --git a/src/command/graph/publish.rs b/src/command/graph/publish.rs index d005b993c1..1633e765eb 100644 --- a/src/command/graph/publish.rs +++ b/src/command/graph/publish.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use rover_client::operations::graph::publish::{self, GraphPublishInput, GraphPublishResponse}; use rover_client::shared::{GitContext, GraphRef}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{parse_schema_source, SchemaSource}; @@ -36,7 +36,7 @@ impl Publish { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -59,7 +59,7 @@ impl Publish { )?; let hash = handle_response(&self.graph, publish_response); - Ok(RoverStdout::SchemaHash(hash)) + Ok(RoverOutput::SchemaHash(hash)) } } diff --git a/src/command/info.rs b/src/command/info.rs index 55756b6e01..47bbdc605b 100644 --- a/src/command/info.rs +++ b/src/command/info.rs @@ -1,4 +1,4 @@ -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; use crate::PKG_VERSION; use serde::Serialize; @@ -9,7 +9,7 @@ use structopt::StructOpt; pub struct Info {} impl Info { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { let os = os_info::get(); // something like "/usr/bin/zsh" or "Unknown" @@ -28,6 +28,6 @@ impl Info { PKG_VERSION, location, os, shell ); - Ok(RoverStdout::None) + Ok(RoverOutput::None) } } diff --git a/src/command/install/mod.rs b/src/command/install/mod.rs index ca2f34be35..60a779df72 100644 --- a/src/command/install/mod.rs +++ b/src/command/install/mod.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use binstall::Installer; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::PKG_NAME; use crate::{anyhow, Context, Result}; use crate::{command::docs::shortlinks, utils::env::RoverEnvKey}; @@ -20,7 +20,7 @@ pub struct Install { } impl Install { - pub fn run(&self, override_install_path: Option) -> Result { + pub fn run(&self, override_install_path: Option) -> Result { let binary_name = PKG_NAME.to_string(); if let Ok(executable_location) = env::current_exe() { let executable_location = Utf8PathBuf::try_from(executable_location)?; @@ -68,7 +68,7 @@ impl Install { } else { eprintln!("{} was not installed. To override the existing installation, you can pass the `--force` flag to the installer.", &binary_name); } - Ok(RoverStdout::None) + Ok(RoverOutput::None) } else { Err(anyhow!("Failed to get the current executable's path.").into()) } diff --git a/src/command/mod.rs b/src/command/mod.rs index 7d0b6c04b4..7cf6e8bd34 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -16,7 +16,7 @@ pub use explain::Explain; pub use graph::Graph; pub use info::Info; pub use install::Install; -pub use output::RoverStdout; +pub use output::RoverOutput; pub use subgraph::Subgraph; pub use supergraph::Supergraph; pub use update::Update; diff --git a/src/command/output.rs b/src/command/output.rs index 141495f8e8..6c720add75 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -8,18 +8,19 @@ use atty::Stream; use crossterm::style::Attribute::Underlined; use rover_client::operations::subgraph::list::SubgraphListResponse; use rover_client::shared::{CheckResponse, FetchResponse, SdlType}; +use serde_json::{json, Value}; use termimad::MadSkin; -/// RoverStdout defines all of the different types of data that are printed -/// to `stdout`. Every one of Rover's commands should return `anyhow::Result` +/// RoverOutput defines all of the different types of data that are printed +/// to `stdout`. Every one of Rover's commands should return `anyhow::Result` /// If the command needs to output some type of data, it should be structured -/// in this enum, and its print logic should be handled in `RoverStdout::print` +/// in this enum, and its print logic should be handled in `RoverOutput::print` /// /// Not all commands will output machine readable information, and those should -/// return `Ok(RoverStdout::None)`. If a new command is added and it needs to +/// return `Ok(RoverOutput::None)`. If a new command is added and it needs to /// return something that is not described well in this enum, it should be added. #[derive(Clone, PartialEq, Debug)] -pub enum RoverStdout { +pub enum RoverOutput { DocsList(HashMap<&'static str, &'static str>), FetchResponse(FetchResponse), CoreSchema(String), @@ -30,14 +31,13 @@ pub enum RoverStdout { Profiles(Vec), Introspection(String), Markdown(String), - PlainText(String), None, } -impl RoverStdout { +impl RoverOutput { pub fn print(&self) { match self { - RoverStdout::DocsList(shortlinks) => { + RoverOutput::DocsList(shortlinks) => { eprintln!( "You can open any of these documentation pages by running {}.\n", Yellow.normal().paint("`rover docs open `") @@ -51,22 +51,22 @@ impl RoverStdout { } println!("{}", table); } - RoverStdout::FetchResponse(fetch_response) => { + RoverOutput::FetchResponse(fetch_response) => { match fetch_response.sdl.r#type { SdlType::Graph | SdlType::Subgraph => print_descriptor("SDL"), SdlType::Supergraph => print_descriptor("Supergraph SDL"), } print_content(&fetch_response.sdl.contents); } - RoverStdout::CoreSchema(csdl) => { + RoverOutput::CoreSchema(csdl) => { print_descriptor("CoreSchema"); print_content(&csdl); } - RoverStdout::SchemaHash(hash) => { + RoverOutput::SchemaHash(hash) => { print_one_line_descriptor("Schema Hash"); print_content(&hash); } - RoverStdout::SubgraphList(details) => { + RoverOutput::SubgraphList(details) => { let mut table = table::get_table(); // bc => sets top row to be bold and center @@ -83,7 +83,7 @@ impl RoverStdout { } else { url }; - let formatted_updated_at: String = if let Some(dt) = subgraph.updated_at { + let formatted_updated_at: String = if let Some(dt) = subgraph.updated_at.local { dt.format("%Y-%m-%d %H:%M:%S %Z").to_string() } else { "N/A".to_string() @@ -98,16 +98,16 @@ impl RoverStdout { details.root_url, details.graph_ref.name ); } - RoverStdout::CheckResponse(check_response) => { + RoverOutput::CheckResponse(check_response) => { print_check_response(check_response); } - RoverStdout::VariantList(variants) => { + RoverOutput::VariantList(variants) => { print_descriptor("Variants"); for variant in variants { println!("{}", variant); } } - RoverStdout::Profiles(profiles) => { + RoverOutput::Profiles(profiles) => { if profiles.is_empty() { eprintln!("No profiles found."); } else { @@ -118,21 +118,44 @@ impl RoverStdout { println!("{}", profile); } } - RoverStdout::Introspection(introspection_response) => { + RoverOutput::Introspection(introspection_response) => { print_descriptor("Introspection Response"); print_content(&introspection_response); } - RoverStdout::Markdown(markdown_string) => { + RoverOutput::Markdown(markdown_string) => { // underline bolded md let mut skin = MadSkin::default(); skin.bold.add_attr(Underlined); println!("{}", skin.inline(&markdown_string)); } - RoverStdout::PlainText(text) => { - println!("{}", text); + RoverOutput::None => (), + } + } + + pub fn get_internal_json(&self) -> Option { + match self { + RoverOutput::DocsList(shortlinks) => { + let mut shortlink_vec = vec![]; + for (shortlink_slug, shortlink_description) in shortlinks { + shortlink_vec.push( + json!({"slug": shortlink_slug, "description": shortlink_description }), + ); + } + Some(json!({ "shortlinks": shortlink_vec })) + } + RoverOutput::FetchResponse(fetch_response) => Some(json!(fetch_response)), + RoverOutput::CoreSchema(csdl) => Some(json!({ "core_schema": csdl })), + RoverOutput::SchemaHash(hash) => Some(json!({ "schema_hash": hash })), + RoverOutput::SubgraphList(list_response) => Some(json!(list_response)), + RoverOutput::CheckResponse(check_response) => Some(json!(check_response)), + RoverOutput::VariantList(variants) => Some(json!({ "variants": variants })), + RoverOutput::Profiles(profiles) => Some(json!({ "profiles": profiles })), + RoverOutput::Introspection(introspection_response) => { + Some(json!({ "introspection_response": introspection_response })) } - RoverStdout::None => (), + RoverOutput::Markdown(markdown_string) => Some(json!({ "markdown": markdown_string })), + RoverOutput::None => None, } } } diff --git a/src/command/subgraph/check.rs b/src/command/subgraph/check.rs index 67201bee45..696ae2b2f6 100644 --- a/src/command/subgraph/check.rs +++ b/src/command/subgraph/check.rs @@ -4,7 +4,7 @@ use structopt::StructOpt; use rover_client::operations::subgraph::check::{self, SubgraphCheckInput}; use rover_client::shared::{CheckConfig, GitContext, GraphRef, ValidationPeriod}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{ @@ -58,7 +58,7 @@ impl Check { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let proposed_schema = load_schema_from_flag(&self.schema, std::io::stdin())?; @@ -83,6 +83,6 @@ impl Check { &client, )?; - Ok(RoverStdout::CheckResponse(res)) + Ok(RoverOutput::CheckResponse(res)) } } diff --git a/src/command/subgraph/delete.rs b/src/command/subgraph/delete.rs index de53867cb9..0e76397e8f 100644 --- a/src/command/subgraph/delete.rs +++ b/src/command/subgraph/delete.rs @@ -2,7 +2,7 @@ use ansi_term::Colour::{Cyan, Red, Yellow}; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -37,7 +37,7 @@ pub struct Delete { } impl Delete { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -65,7 +65,7 @@ impl Delete { // I chose not to error here, since this is a perfectly valid path if !confirm_delete()? { eprintln!("Delete cancelled by user"); - return Ok(RoverStdout::None); + return Ok(RoverOutput::None); } } @@ -79,7 +79,7 @@ impl Delete { )?; handle_response(delete_response, &self.subgraph, &graph_ref); - Ok(RoverStdout::None) + Ok(RoverOutput::None) } } diff --git a/src/command/subgraph/fetch.rs b/src/command/subgraph/fetch.rs index ba7f35bc70..92643e71bc 100644 --- a/src/command/subgraph/fetch.rs +++ b/src/command/subgraph/fetch.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use rover_client::operations::subgraph::fetch::{self, SubgraphFetchInput}; use rover_client::shared::GraphRef; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -29,7 +29,7 @@ pub struct Fetch { } impl Fetch { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -47,6 +47,6 @@ impl Fetch { &client, )?; - Ok(RoverStdout::FetchResponse(fetch_response)) + Ok(RoverOutput::FetchResponse(fetch_response)) } } diff --git a/src/command/subgraph/introspect.rs b/src/command/subgraph/introspect.rs index bdce9cdead..5b527495d3 100644 --- a/src/command/subgraph/introspect.rs +++ b/src/command/subgraph/introspect.rs @@ -9,7 +9,7 @@ use rover_client::{ operations::subgraph::introspect::{self, SubgraphIntrospectInput}, }; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::parsers::parse_header; use crate::Result; @@ -36,7 +36,7 @@ pub struct Introspect { } impl Introspect { - pub fn run(&self, client: Client) -> Result { + pub fn run(&self, client: Client) -> Result { let client = GraphQLClient::new(&self.endpoint.to_string(), client)?; // add the flag headers to a hashmap to pass along to rover-client @@ -49,6 +49,6 @@ impl Introspect { let introspection_response = introspect::run(SubgraphIntrospectInput { headers }, &client)?; - Ok(RoverStdout::Introspection(introspection_response.result)) + Ok(RoverOutput::Introspection(introspection_response.result)) } } diff --git a/src/command/subgraph/list.rs b/src/command/subgraph/list.rs index 4bcbc312c4..c1794801e8 100644 --- a/src/command/subgraph/list.rs +++ b/src/command/subgraph/list.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use rover_client::operations::subgraph::list::{self, SubgraphListInput}; use rover_client::shared::GraphRef; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -24,7 +24,7 @@ pub struct List { } impl List { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; eprintln!( @@ -40,6 +40,6 @@ impl List { &client, )?; - Ok(RoverStdout::SubgraphList(list_details)) + Ok(RoverOutput::SubgraphList(list_details)) } } diff --git a/src/command/subgraph/mod.rs b/src/command/subgraph/mod.rs index ee1ff37048..ab238bed8a 100644 --- a/src/command/subgraph/mod.rs +++ b/src/command/subgraph/mod.rs @@ -8,7 +8,7 @@ mod publish; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -47,7 +47,7 @@ impl Subgraph { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { match &self.command { Command::Publish(command) => command.run(client_config, git_context), Command::Introspect(command) => command.run(client_config.get_reqwest_client()), diff --git a/src/command/subgraph/publish.rs b/src/command/subgraph/publish.rs index 5ee6953590..fb3a51d98f 100644 --- a/src/command/subgraph/publish.rs +++ b/src/command/subgraph/publish.rs @@ -2,7 +2,7 @@ use ansi_term::Colour::{Cyan, Red, Yellow}; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::{ client::StudioClientConfig, loaders::load_schema_from_flag, @@ -56,7 +56,7 @@ impl Publish { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = format!("{}:{}", &self.graph.name, &self.graph.variant); eprintln!( @@ -83,7 +83,7 @@ impl Publish { )?; handle_publish_response(publish_response, &self.subgraph, &self.graph.name); - Ok(RoverStdout::None) + Ok(RoverOutput::None) } } diff --git a/src/command/supergraph/compose/do_compose.rs b/src/command/supergraph/compose/do_compose.rs index 63086383c5..4bbbdf2f5e 100644 --- a/src/command/supergraph/compose/do_compose.rs +++ b/src/command/supergraph/compose/do_compose.rs @@ -1,6 +1,6 @@ use crate::command::supergraph::config::{self, SchemaSource, SupergraphConfig}; use crate::utils::client::StudioClientConfig; -use crate::{anyhow, command::RoverStdout, error::RoverError, Result, Suggestion}; +use crate::{anyhow, command::RoverOutput, error::RoverError, Result, Suggestion}; use ansi_term::Colour::Red; use camino::Utf8PathBuf; @@ -32,7 +32,7 @@ pub struct Compose { } impl Compose { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let supergraph_config = config::parse_supergraph_config(&self.config_path)?; let subgraph_definitions = get_subgraph_definitions( supergraph_config, @@ -42,7 +42,7 @@ impl Compose { )?; match harmonizer::harmonize(subgraph_definitions) { - Ok(core_schema) => Ok(RoverStdout::CoreSchema(core_schema)), + Ok(core_schema) => Ok(RoverOutput::CoreSchema(core_schema)), Err(composition_errors) => { let num_failures = composition_errors.len(); for composition_error in composition_errors { diff --git a/src/command/supergraph/compose/no_compose.rs b/src/command/supergraph/compose/no_compose.rs index bc9c6904ee..dea61423f0 100644 --- a/src/command/supergraph/compose/no_compose.rs +++ b/src/command/supergraph/compose/no_compose.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use crate::utils::client::StudioClientConfig; use crate::{ anyhow, - command::RoverStdout, + command::RoverOutput, error::{RoverError, Suggestion}, Result, }; @@ -24,7 +24,7 @@ pub struct Compose { } impl Compose { - pub fn run(&self, _client_config: StudioClientConfig) -> Result { + pub fn run(&self, _client_config: StudioClientConfig) -> Result { let mut err = RoverError::new(anyhow!( "This version of Rover does not support this command." )); diff --git a/src/command/supergraph/fetch.rs b/src/command/supergraph/fetch.rs index 878b92bd53..793af1f87d 100644 --- a/src/command/supergraph/fetch.rs +++ b/src/command/supergraph/fetch.rs @@ -1,5 +1,5 @@ use crate::utils::client::StudioClientConfig; -use crate::{command::RoverStdout, Result}; +use crate::{command::RoverOutput, Result}; use rover_client::operations::supergraph::fetch::{self, SupergraphFetchInput}; use rover_client::shared::GraphRef; @@ -23,7 +23,7 @@ pub struct Fetch { } impl Fetch { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -39,6 +39,6 @@ impl Fetch { &client, )?; - Ok(RoverStdout::FetchResponse(fetch_response)) + Ok(RoverOutput::FetchResponse(fetch_response)) } } diff --git a/src/command/supergraph/mod.rs b/src/command/supergraph/mod.rs index 8b575b5fac..51e2141a3d 100644 --- a/src/command/supergraph/mod.rs +++ b/src/command/supergraph/mod.rs @@ -5,7 +5,7 @@ mod fetch; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -26,7 +26,7 @@ pub enum Command { } impl Supergraph { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { match &self.command { Command::Fetch(command) => command.run(client_config), Command::Compose(command) => command.run(client_config), diff --git a/src/command/update/check.rs b/src/command/update/check.rs index f4fe31ebd4..b380968555 100644 --- a/src/command/update/check.rs +++ b/src/command/update/check.rs @@ -2,7 +2,7 @@ use reqwest::blocking::Client; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::{utils::version, Result}; use houston as config; @@ -13,8 +13,8 @@ pub struct Check { } impl Check { - pub fn run(&self, config: config::Config, client: Client) -> Result { + pub fn run(&self, config: config::Config, client: Client) -> Result { version::check_for_update(config, true, client)?; - Ok(RoverStdout::None) + Ok(RoverOutput::None) } } diff --git a/src/command/update/mod.rs b/src/command/update/mod.rs index 9296ba172f..8964ff90ee 100644 --- a/src/command/update/mod.rs +++ b/src/command/update/mod.rs @@ -4,7 +4,7 @@ use reqwest::blocking::Client; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; use houston as config; @@ -22,7 +22,7 @@ pub enum Command { } impl Update { - pub fn run(&self, config: config::Config, client: Client) -> Result { + pub fn run(&self, config: config::Config, client: Client) -> Result { match &self.command { Command::Check(command) => command.run(config, client), } diff --git a/src/error/metadata/mod.rs b/src/error/metadata/mod.rs index 8969d419d8..e3665281c4 100644 --- a/src/error/metadata/mod.rs +++ b/src/error/metadata/mod.rs @@ -7,19 +7,26 @@ pub use suggestion::Suggestion; use houston::HoustonProblem; use rover_client::RoverClientError; -use crate::{command::output::print_check_response, utils::env::RoverEnvKey}; +use crate::{ + command::output::print_check_response, + utils::{env::RoverEnvKey, stringify::option_from_display}, +}; use std::{env, fmt::Display}; use ansi_term::Colour::Red; +use serde::Serialize; /// Metadata contains extra information about specific errors /// Currently this includes an optional error `Code` /// and an optional `Suggestion` -#[derive(Default, Debug)] +#[derive(Default, Serialize, Debug)] pub struct Metadata { + #[serde(serialize_with = "option_from_display")] pub suggestion: Option, pub code: Option, + + #[serde(skip_serializing)] pub is_parse_error: bool, } diff --git a/src/error/metadata/suggestion.rs b/src/error/metadata/suggestion.rs index 64cc73186c..5cd0656bdd 100644 --- a/src/error/metadata/suggestion.rs +++ b/src/error/metadata/suggestion.rs @@ -6,8 +6,10 @@ use rover_client::shared::GraphRef; use crate::utils::env::RoverEnvKey; +use serde::Serialize; + /// `Suggestion` contains possible suggestions for remedying specific errors. -#[derive(Debug)] +#[derive(Serialize, Debug)] pub enum Suggestion { SubmitIssue, SetConfigHome, diff --git a/src/error/mod.rs b/src/error/mod.rs index 1da93d72b2..639737a9a3 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -6,18 +6,24 @@ pub(crate) use metadata::Metadata; pub type Result = std::result::Result; use ansi_term::Colour::{Cyan, Red}; +use serde::Serialize; use std::borrow::BorrowMut; use std::fmt::{self, Debug, Display}; pub use self::metadata::Suggestion; +use crate::utils::stringify::from_display; + /// A specialized `Error` type for Rover that wraps `anyhow` /// and provides some extra `Metadata` for end users depending /// on the specific error they encountered. -#[derive(Debug)] +#[derive(Serialize, Debug)] pub struct RoverError { + #[serde(rename(serialize = "message"), serialize_with = "from_display")] error: anyhow::Error, + + #[serde(flatten)] metadata: Metadata, } diff --git a/src/utils/stringify.rs b/src/utils/stringify.rs index 6bd57471ed..3d45e594c7 100644 --- a/src/utils/stringify.rs +++ b/src/utils/stringify.rs @@ -2,20 +2,30 @@ //! a struct with the `Display`/`FromStr` implementations //! if it does not implement `Serialize`/`Deserialize` //! code taken from this: https://github.com/serde-rs/serde/issues/1316 -//! and can be used by annotating a field with -//! #[serde(serialize_with = "from_display")] +//! and can be used by annotating a field with either +//! #[serde(serialize_with = "from_display")] or +//! #[serde(serialize_with = "option_from_display")] +//! depending on if the type you're serializing is nested in an Option use std::fmt::Display; use serde::Serializer; -pub fn from_display(value: &Option, serializer: S) -> Result +pub fn option_from_display(value: &Option, serializer: S) -> Result where T: Display, S: Serializer, { if let Some(value) = value { - serializer.collect_str(value) + from_display(value, serializer) } else { serializer.serialize_none() } } + +pub fn from_display(value: &T, serializer: S) -> Result +where + T: Display, + S: Serializer, +{ + serializer.collect_str(value) +}