diff --git a/CHANGELOG.md b/CHANGELOG.md index daa390e71..d7f2a1a58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## 📚 Documentation --> +# [0.10.1] - 2022-11-28 + +## 🚀 Features + +- **Replace the '--output' option type with '--format' - @gocamille, #1413 fixes #1212** + + This change adds the new option, `--format`, to allow users to define the format type for messages printed to `stdout` (either by passing `plain` or `json` as an argument to `--format`). This replaces the use of `--output` for defining format types. The `--output` option will be available to define the output file type instead, following [Command Line Interface Guidelines for file outputs](https://clig.dev/#:~:text=%2Do%2C%20%2D%2Doutput%3A%20Output%20file.%20For%20example%2C%20sort%2C%20gcc.). This is an additive, non-breaking change and using the `--output` option will continue to be valid. + + # [0.10.0] - 2022-11-10 > Important: 1 potentially breaking change below, indicated by **❗ BREAKING ❗** diff --git a/README.md b/README.md index 1c7003de7..fa43e246c 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,17 @@ Options: -l, --log Specify Rover's log level + --format + Specify Rover's format type + + [default: plain] + [possible values: plain, json] + --output Specify Rover's output type [default: plain] - [possible values: plain, json] + [possible values: plain, json, filename] --insecure-accept-invalid-certs Accept invalid certificates when performing HTTPS requests. diff --git a/crates/rover-std/src/emoji.rs b/crates/rover-std/src/emoji.rs index 8b75169db..cf000c12b 100644 --- a/crates/rover-std/src/emoji.rs +++ b/crates/rover-std/src/emoji.rs @@ -20,6 +20,7 @@ pub enum Emoji { Skull, Compose, Warn, + Memo, } impl Emoji { @@ -42,6 +43,7 @@ impl Emoji { Skull => "💀 ", Compose => "🎶 ", Warn => "⚠️ ", + Memo => "📝 ", } } } diff --git a/docs/source/commands/graphs.mdx b/docs/source/commands/graphs.mdx index 937ae7dcb..3f12deadf 100644 --- a/docs/source/commands/graphs.mdx +++ b/docs/source/commands/graphs.mdx @@ -63,7 +63,7 @@ You can also save the output to a local `.graphql` file like so: ```bash # Creates prod-schema.graphql or overwrites if it already exists -rover graph fetch my-graph@my-variant > prod-schema.graphql +rover graph fetch my-graph@my-variant --output prod-schema.graphql ``` > For more on passing values via `stdout`, see [Conventions](../conventions#using-stdout). diff --git a/docs/source/commands/readmes.mdx b/docs/source/commands/readmes.mdx index 5250d70b8..fb8af6583 100644 --- a/docs/source/commands/readmes.mdx +++ b/docs/source/commands/readmes.mdx @@ -29,13 +29,13 @@ By default, the README is output to `stdout`. You can also save the output to a ```bash # Creates README.md or overwrites if it already exists -rover readme fetch my-graph@my-variant > README.md +rover readme fetch my-graph@my-variant --output README.md ``` -You can also request the output as JSON with the `--output json` option: +You can also request the output as JSON with the `--format json` option: ```bash -rover readme fetch my-graph@my-variant --output json +rover readme fetch my-graph@my-variant --format json ``` > For more on passing values via `stdout`, see [Conventions](../conventions#using-stdout). diff --git a/docs/source/commands/subgraphs.mdx b/docs/source/commands/subgraphs.mdx index 52ca89326..6e9fd2adf 100644 --- a/docs/source/commands/subgraphs.mdx +++ b/docs/source/commands/subgraphs.mdx @@ -54,7 +54,7 @@ The subgraph must be reachable by Rover. The subgraph does _not_ need to have in #### Watching for schema changes -If you pass `--watch` to `rover subgraph introspect`, Rover introspects your subgraph every second. Whenever the returned schema differs from the _previously_ returned schema, Rover outputs the updated schema. +If you pass `--watch` to `rover subgraph introspect`, Rover introspects your subgraph every second. Whenever the returned schema differs from the _previously_ returned schema, Rover outputs the updated schema. This is most useful when combined with the `--output ` argument which will write the introspection response out to a file whenever its contents change. #### Including headers @@ -83,7 +83,7 @@ You can also save the output to a local `.graphql` file like so: ```bash # Creates accounts-schema.graphql or overwrites if it already exists -rover subgraph introspect http://localhost:4000 > accounts-schema.graphql +rover subgraph introspect http://localhost:4000 --output accounts-schema.graphql ``` > For more on passing values via `stdout`, see [Using `stdout`](../conventions#using-stdout). diff --git a/docs/source/commands/supergraphs.mdx b/docs/source/commands/supergraphs.mdx index b183a01fd..0eff6e2a9 100644 --- a/docs/source/commands/supergraphs.mdx +++ b/docs/source/commands/supergraphs.mdx @@ -105,7 +105,7 @@ You can save the schema output to a local `.graphql` file like so: ```bash # Creates prod-schema.graphql or overwrites if it already exists -rover supergraph compose --config ./supergraph.yaml > prod-schema.graphql +rover supergraph compose --config ./supergraph.yaml --output prod-schema.graphql ``` > For more on passing values via `stdout`, see [Using `stdout`](../conventions#using-stdout). diff --git a/docs/source/configuring.md b/docs/source/configuring.md index ce242d4df..22957cd0c 100644 --- a/docs/source/configuring.md +++ b/docs/source/configuring.md @@ -74,17 +74,24 @@ rover graph check my-graph@prod --schema ./schema.graphql --log debug If Rover log messages are unhelpful or unclear, please leave us feedback in an [issue on GitHub](https://github.com/apollographql/rover/issues/new/choose)! -## Output format - -### `--output plain` (default) +## Configuring output By default, Rover prints the main output of its commands to `stdout` in plaintext. It also prints a _descriptor_ for that output to `stderr` if it thinks it's being operated by a human (it checks whether the terminal is TTY). -> For more on `stdout`, see [Conventions](./conventions/#using-stdout). +> For more on `stdout`, see [Conventions](https://chat.openai.com/conventions/#using-stdout). + +Every Rover command supports two options for configuring its output behavior: + +- `--format`, for [setting the output format](#setting-output-format) (`plain` or `json`) +- `--output`, for [writing a command's output to a file](#setting-output-location) instead of `stdout` -### `--output json` +### JSON output -For more programmatic control over Rover's output, you can pass `--output json` to any command. Rover JSON output has the following minimal structure: +> **Note:** The `--format` option was added in Rover v0.11.0. Earlier versions of Rover use the `--output` option to set output format. +> +> Current versions of Rover still support using `--output` this way, but that support is deprecated and will be removed in a future release. + +For more programmatic control over Rover's output, you can pass `--format json` to any command. Rover JSON output has the following minimal structure: ```json title="success_example" { @@ -232,7 +239,21 @@ This particular `error` object includes `details` about what went wrong. Notice #### Example `jq` script -You can combine the `--output json` flag with the [`jq`](https://stedolan.github.io/jq/) command line tool to create powerful custom workflows. For example, [this gist](https://gist.github.com/EverlastingBugstopper/d6aa0d9a49bcf39f2df53e1cfb9bb88a) demonstrates converting output from `rover {sub}graph check my-graph --output json` to Markdown. +You can combine the `--format json` flag with the [`jq`](https://stedolan.github.io/jq/) command line tool to create powerful custom workflows. For example, [this gist](https://gist.github.com/EverlastingBugstopper/d6aa0d9a49bcf39f2df53e1cfb9bb88a) demonstrates converting output from `rover {sub}graph check my-graph --format json` to Markdown. + +### Writing to a file + +The `--output` option enables you to specify a file destination for writing a Rover command's output: + +```bash +rover supergraph compose --output ./supergraph-schema.graphql --config ./supergraph.yaml +``` + +If the specified file already exists, Rover overwrites it. + +> **Note:** This functionality is available in Rover v0.11.0 and later. In _earlier_ versions of Rover, the `--output` option instead provides the functionality that's now provided by the [`--format` option](#json-output). +> +> Current versions of Rover still support using `--output` like `--format`, but that support is deprecated and will be removed in a future release. ## Setting config storage location diff --git a/docs/source/conventions.md b/docs/source/conventions.md index 8fcfe050a..375a6cf8a 100644 --- a/docs/source/conventions.md +++ b/docs/source/conventions.md @@ -39,9 +39,9 @@ All Rover commands that interact with the Apollo graph registry require a graph ### Using `stdout` -Rover commands print to `stdout` in a predictable, portable format. This enables output to be used elsewhere (such as in another CLI, or as input to another Rover command). To help maintain this predictability, Rover prints logs to `stderr` instead of `stdout`. +Rover commands print to `stdout` in a predictable, portable format. This enables output to be used elsewhere (such as in another CLI, or as input to another Rover command). To help maintain this predictability, Rover prints progress logs to `stderr` instead of `stdout`. -To redirect Rover's output to a location other than your terminal, you can use the pipe `|` or output redirect `>` operators. +To redirect Rover's output to a location other than your terminal, you can use the `--output ` argument, the pipe `|` operator, or the redirect `>` operator. #### Pipe `|` @@ -53,12 +53,12 @@ rover graph introspect http://localhost:4000 | pbcopy In this example, the output of the `introspect` command is piped to `pbcopy`, a MacOS command that copies a value to the clipboard. Certain Rover commands also accept values from `stdin`, as explained in [Using `stdin`](#using-stdin). -#### Output redirect `>` +#### Output to a file -Use the output redirect operator to write the `stdout` of a command to a file, like so: +Use the `--output ` argument to write command output to a file. ``` -rover graph fetch my-graph@prod > schema.graphql +rover graph fetch my-graph@prod --output schema.graphql ``` In this example, the schema returned by `graph fetch` is written to the file `schema.graphql`. If this file already exists, it's overwritten. Otherwise, it's created. @@ -72,5 +72,3 @@ rover graph introspect http://localhost:4000 | rover graph check my-graph --sche ``` In this example, the schema returned by `graph introspect` is then passed as the `--schema` option to `graph check`. - -> Currently, `--schema` is the only Rover option that accepts a file path. diff --git a/docs/source/migration.md b/docs/source/migration.md index 43a63ea60..4943e81d6 100644 --- a/docs/source/migration.md +++ b/docs/source/migration.md @@ -144,9 +144,9 @@ As a workaround, you might be able to use `cat` to combine multiple files and pa ### Machine-readable output -In the Apollo CLI, many commands support alternate output formatting options, such as `--json` and `--markdown`. Currently, Rover only supports `--output json`, and leaves markdown formatting up to the consumer. For more information on JSON output in Rover, see [these docs](./configuring/#--output-json). +In the Apollo CLI, many commands support alternate output formatting options, such as `--json` and `--markdown`. Currently, Rover only supports `--format json`, and leaves markdown formatting up to the consumer. For more information on JSON output in Rover, see [these docs](./configuring/#json-output). -An example script for converting output from `rover {sub}graph check my-graph --output json` can be found in [this gist](https://gist.github.com/EverlastingBugstopper/d6aa0d9a49bcf39f2df53e1cfb9bb88a). +An example script for converting output from `rover {sub}graph check my-graph --format json` can be found in [this gist](https://gist.github.com/EverlastingBugstopper/d6aa0d9a49bcf39f2df53e1cfb9bb88a). ## Examples @@ -160,8 +160,8 @@ An example script for converting output from `rover {sub}graph check my-graph -- apollo client:download-schema --graph my-graph --variant prod ## Rover ## -# automatically outputs to stdout. Can redirect to schema.graphql -rover graph fetch my-graph@prod > schema.graphql +# automatically outputs to stdout. Can redirect to schema.graphql with the `--output ` argument +rover graph fetch my-graph@prod --output schema.graphql ``` ### Fetching a graph's schema from introspection @@ -173,9 +173,9 @@ rover graph fetch my-graph@prod > schema.graphql apollo service:download --endpoint http://localhost:4000 ## Rover ## -# automatically outputs to stdout. Can redirect to schema.graphql +# automatically outputs to stdout. Can redirect to schema.graphql with the `--output ` argument # can ONLY output SDL -rover graph introspect http://localhost:4000 +rover graph introspect http://localhost:4000 --output schema.graphql ``` ### Publishing a monolithic schema to the Apollo graph registry @@ -261,8 +261,6 @@ rover graph introspect http://localhost:4001 --header "authorization: Bearer wxy | rover graph check my-graph@prod --schema - ``` - - ### Pushing Subgraph Changes (with a config file) ```js diff --git a/src/bin/rover.rs b/src/bin/rover.rs index c364f6834..40ead2101 100644 --- a/src/bin/rover.rs +++ b/src/bin/rover.rs @@ -2,7 +2,7 @@ use robot_panic::setup_panic; use rover::cli::Rover; #[calm_io::pipefail] -fn main() -> std::io::Result<()> { +fn main() -> Result<_, std::io::Error> { setup_panic!(Metadata { name: rover::PKG_NAME.into(), version: rover::PKG_VERSION.into(), @@ -10,5 +10,5 @@ fn main() -> std::io::Result<()> { homepage: rover::PKG_HOMEPAGE.into(), repository: rover::PKG_REPOSITORY.into() }); - Rover::run_from_args() + Ok(Rover::run_from_args()) } diff --git a/src/cli.rs b/src/cli.rs index be4b24b16..9c1b98746 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,8 +4,8 @@ use lazycell::{AtomicLazyCell, LazyCell}; use reqwest::blocking::Client; use serde::Serialize; -use crate::command::output::JsonOutput; use crate::command::{self, RoverOutput}; +use crate::options::OutputOpts; use crate::utils::{ client::{ClientBuilder, ClientTimeout, StudioClientConfig}, env::{RoverEnv, RoverEnvKey}, @@ -61,9 +61,8 @@ pub struct Rover { #[serde(serialize_with = "option_from_display")] log_level: Option, - /// Specify Rover's output type - #[arg(long = "output", default_value = "plain", global = true)] - output_type: OutputType, + #[clap(flatten)] + output_opts: OutputOpts, /// Accept invalid certificates when performing HTTPS requests. /// @@ -110,13 +109,14 @@ pub struct Rover { } impl Rover { - pub fn run_from_args() -> io::Result<()> { + pub fn run_from_args() -> RoverResult<()> { Rover::parse().run() } - pub fn run(&self) -> io::Result<()> { + pub fn run(&self) -> RoverResult<()> { timber::init(self.log_level); tracing::trace!(command_structure = ?self); + self.output_opts.validate_options(); // attempt to create a new `Session` to capture anonymous usage data let rover_output = match Session::new(self) { @@ -152,20 +152,13 @@ impl Rover { match rover_output { Ok(output) => { - match self.output_type { - OutputType::Plain => output.print()?, - OutputType::Json => JsonOutput::from(output).print()?, - } + self.output_opts.handle_output(output)?; + process::exit(0); } Err(error) => { - match self.output_type { - OutputType::Json => JsonOutput::from(error).print()?, - OutputType::Plain => { - tracing::debug!(?error); - error.print()?; - } - } + self.output_opts.handle_output(error)?; + process::exit(1); } } @@ -200,7 +193,7 @@ impl Rover { self.get_client_config()?, self.get_git_context()?, self.get_checks_timeout_seconds()?, - self.get_json(), + &self.output_opts, ), Command::Template(command) => command.run(self.get_client_config()?), Command::Readme(command) => command.run(self.get_client_config()?), @@ -208,7 +201,7 @@ impl Rover { self.get_client_config()?, self.get_git_context()?, self.get_checks_timeout_seconds()?, - self.get_json(), + &self.output_opts, ), Command::Update(command) => { command.run(self.get_rover_config()?, self.get_reqwest_client()?) @@ -221,10 +214,6 @@ impl Rover { } } - pub(crate) fn get_json(&self) -> bool { - matches!(self.output_type, OutputType::Json) - } - pub(crate) fn get_rover_config(&self) -> RoverResult { let override_home: Option = self .get_env_var(RoverEnvKey::ConfigHome)? @@ -415,13 +404,19 @@ pub enum Command { } #[derive(ValueEnum, Debug, Serialize, Clone, Eq, PartialEq)] -pub enum OutputType { +pub enum RoverOutputFormatKind { Plain, Json, } -impl Default for OutputType { +#[derive(ValueEnum, Debug, Serialize, Clone, Eq, PartialEq)] +pub enum RoverOutputKind { + RoverOutput, + RoverError, +} + +impl Default for RoverOutputFormatKind { fn default() -> Self { - OutputType::Plain + RoverOutputFormatKind::Plain } } diff --git a/src/command/graph/introspect.rs b/src/command/graph/introspect.rs index 4abeff467..fd569d369 100644 --- a/src/command/graph/introspect.rs +++ b/src/command/graph/introspect.rs @@ -8,7 +8,10 @@ use rover_client::{ operations::graph::introspect::{self, GraphIntrospectInput}, }; -use crate::{options::IntrospectOpts, RoverOutput, RoverResult}; +use crate::{ + options::{IntrospectOpts, OutputOpts}, + RoverOutput, RoverResult, +}; #[derive(Debug, Serialize, Parser)] pub struct Introspect { @@ -17,10 +20,9 @@ pub struct Introspect { } impl Introspect { - pub fn run(&self, client: Client, json: bool) -> RoverResult { + pub fn run(&self, client: Client, output_opts: &OutputOpts) -> RoverResult { if self.opts.watch { - self.exec_and_watch(&client, json)?; - Ok(RoverOutput::EmptySuccess) + self.exec_and_watch(&client, output_opts) } else { let sdl = self.exec(&client, true)?; Ok(RoverOutput::Introspection(sdl)) @@ -41,9 +43,8 @@ impl Introspect { Ok(introspect::run(GraphIntrospectInput { headers }, &client, should_retry)?.schema_sdl) } - pub fn exec_and_watch(&self, client: &Client, json: bool) -> RoverResult { + pub fn exec_and_watch(&self, client: &Client, output_opts: &OutputOpts) -> ! { self.opts - .exec_and_watch(|| self.exec(client, false), json)?; - Ok(RoverOutput::EmptySuccess) + .exec_and_watch(|| self.exec(client, false), output_opts) } } diff --git a/src/command/graph/mod.rs b/src/command/graph/mod.rs index bb91cc54a..1ac9bf3a3 100644 --- a/src/command/graph/mod.rs +++ b/src/command/graph/mod.rs @@ -13,6 +13,7 @@ pub use publish::Publish; use clap::Parser; use serde::Serialize; +use crate::options::OutputOpts; use crate::utils::client::StudioClientConfig; use crate::{RoverOutput, RoverResult}; @@ -49,7 +50,7 @@ impl Graph { client_config: StudioClientConfig, git_context: GitContext, checks_timeout_seconds: u64, - json: bool, + output_opts: &OutputOpts, ) -> RoverResult { match &self.command { Command::Check(command) => { @@ -58,7 +59,9 @@ impl Graph { Command::Delete(command) => command.run(client_config), Command::Fetch(command) => command.run(client_config), Command::Publish(command) => command.run(client_config, git_context), - Command::Introspect(command) => command.run(client_config.get_reqwest_client()?, json), + Command::Introspect(command) => { + command.run(client_config.get_reqwest_client()?, output_opts) + } } } } diff --git a/src/command/output.rs b/src/command/output.rs index 4fd9d9798..5b2f411a8 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -1,14 +1,15 @@ use std::collections::BTreeMap; -use std::fmt::{self, Debug, Display}; +use std::fmt::Debug; use std::io; use crate::command::supergraph::compose::CompositionOutput; +use crate::options::JsonVersion; use crate::utils::table::{self, row}; use crate::RoverError; use crate::options::GithubTemplate; use atty::Stream; -use calm_io::{stderr, stderrln, stdout, stdoutln}; +use calm_io::{stderr, stderrln}; use camino::Utf8PathBuf; use crossterm::style::Attribute::Underlined; use rover_client::operations::contract::describe::ContractDescribeResponse; @@ -22,14 +23,13 @@ use rover_client::shared::{ }; use rover_client::RoverClientError; use rover_std::Style; -use serde::Serialize; use serde_json::{json, Value}; use termimad::MadSkin; /// RoverOutput defines all of the different types of data that are printed /// to `stdout`. Every one of Rover's commands should return `saucer::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 `RoverOutput::print` +/// in this enum, and its print logic should be handled in `RoverOutput::get_stdout` /// /// Not all commands will output machine readable information, and those should /// return `Ok(RoverOutput::EmptySuccess)`. If a new command is added and it needs to @@ -40,7 +40,7 @@ pub enum RoverOutput { ContractPublish(ContractPublishResponse), DocsList(BTreeMap<&'static str, &'static str>), FetchResponse(FetchResponse), - CoreSchema(String), + SupergraphSchema(String), CompositionResult(CompositionOutput), SubgraphList(SubgraphListResponse), CheckResponse(CheckResponse), @@ -82,28 +82,20 @@ pub enum RoverOutput { } impl RoverOutput { - pub fn print(&self) -> io::Result<()> { - match self { + pub fn get_stdout(&self) -> io::Result> { + Ok(match self { RoverOutput::ContractDescribe(describe_response) => { - print_descriptor("Configuration Description")?; - print_content(&describe_response.description)?; - stderrln!( - "View the variant's full configuration at {}", - Style::Link.paint(format!( - "{}/graph/{}/settings/variant?variant={}", - describe_response.root_url, - describe_response.graph_ref.name, - describe_response.graph_ref.variant, - )) - )?; + Some(format!("{description}\nView the variant's full configuration at {variant_config}", description = &describe_response.description, variant_config = + Style::Link.paint(format!( + "{}/graph/{}/settings/variant?variant={}", + describe_response.root_url, + describe_response.graph_ref.name, + describe_response.graph_ref.variant, + )))) } RoverOutput::ContractPublish(publish_response) => { - print_descriptor("New Configuration Description")?; - print_content(&publish_response.config_description)?; - match &publish_response.launch_cli_copy { - Some(launch_cli_copy) => stderrln!("{}", launch_cli_copy)?, - None => stderrln!("No launch was triggered for this publish.")?, - } + let launch_cli_copy = publish_response.launch_cli_copy.clone().unwrap_or_else(|| "No launch was triggered for this publish.".to_string()); + Some(format!("{description}\n{launch_cli_copy}", description = &publish_response.config_description)) } RoverOutput::DocsList(shortlinks) => { stderrln!( @@ -117,14 +109,10 @@ impl RoverOutput { for (shortlink_slug, shortlink_description) in shortlinks { table.add_row(row![shortlink_slug, shortlink_description]); } - stdoutln!("{}", table)?; + Some(format!("{}", table)) } 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)?; + Some((fetch_response.sdl.contents).to_string()) } RoverOutput::GraphPublishResponse { graph_ref, @@ -136,8 +124,7 @@ impl RoverOutput { publish_response.api_schema_hash, publish_response.change_summary )?; - print_one_line_descriptor("Schema Hash")?; - print_content(&publish_response.api_schema_hash)?; + Some((publish_response.api_schema_hash).to_string()) } RoverOutput::SubgraphPublishResponse { graph_ref, @@ -172,6 +159,7 @@ impl RoverOutput { stderrln!("{} The following build errors occurred:", warn_prefix)?; stderrln!("{}", &publish_response.build_errors)?; } + None } RoverOutput::SubgraphDeleteResponse { graph_ref, @@ -193,21 +181,22 @@ impl RoverOutput { stderrln!("{} This is only a prediction. If the graph changes before confirming, these errors could change.", warn_prefix)?; } else { stderrln!("{} At the time of checking, there would be no build errors resulting from the deletion of this subgraph.", warn_prefix)?; - stderrln!("{} This is only a prediction. If the graph changes before confirming, there could be build errors.", warn_prefix)? + stderrln!("{} This is only a prediction. If the graph changes before confirming, there could be build errors.", warn_prefix)?; } + None } else { if delete_response.supergraph_was_updated { stderrln!( "The '{}' subgraph was removed from '{}'. The remaining subgraphs were composed.", Style::Link.paint(subgraph), Style::Link.paint(graph_ref.to_string()), - )? + )?; } else { stderrln!( "{} The supergraph schema for '{}' was not updated. See errors below.", warn_prefix, Style::Link.paint(graph_ref.to_string()) - )? + )?; } if !delete_response.build_errors.is_empty() { @@ -220,12 +209,10 @@ impl RoverOutput { stderrln!("{}", &delete_response.build_errors)?; } + None } } - RoverOutput::CoreSchema(csdl) => { - print_descriptor("CoreSchema")?; - print_content(csdl)?; - } + RoverOutput::SupergraphSchema(csdl) => Some((csdl).to_string()), RoverOutput::CompositionResult(composition_output) => { let warn_prefix = Style::HintPrefix.paint("HINT:"); @@ -237,8 +224,7 @@ impl RoverOutput { stderrln!("{}", hints_string)?; - print_descriptor("CoreSchema")?; - print_content(&composition_output.supergraph_sdl)?; + Some((composition_output.supergraph_sdl).to_string()) } RoverOutput::SubgraphList(details) => { let mut table = table::get_table(); @@ -265,13 +251,10 @@ impl RoverOutput { table.add_row(row![subgraph.name, url, formatted_updated_at]); } - - stdoutln!("{}", table)?; - stdoutln!( - "View full details at {}/graph/{}/service-list", - details.root_url, - details.graph_ref.name - )?; + Some(format!( + "{}/n View full details at {}/graph/{}/service-list", + table, details.root_url, details.graph_ref.name + )) } RoverOutput::TemplateList(templates) => { let mut table = table::get_table(); @@ -288,77 +271,58 @@ impl RoverOutput { ]); } - stdoutln!("{}", table)?; + Some(format!("{}", table)) } RoverOutput::TemplateUseSuccess { template, path } => { - print_descriptor("Project generated")?; - stdoutln!( - "Successfully created a new project from the '{template_id}' template in {path}", - template_id = Style::Command.paint(template.id), - path = Style::Path.paint(path.as_str()) - )?; - stdoutln!( - "Read the generated '{readme}' file for next steps.", - readme = Style::Path.paint("README.md") - )?; + let template_id = Style::Command.paint(template.id); + let path = Style::Path.paint(path.as_str()); + let readme = Style::Path.paint("README.md"); let forum_call_to_action = Style::CallToAction.paint( "Have a question or suggestion about templates? Let us know at \ https://community.apollographql.com", ); - stdoutln!("{}", forum_call_to_action)?; - } - RoverOutput::CheckResponse(check_response) => { - print_descriptor("Check Result")?; - print_content(check_response.get_table())?; - } - RoverOutput::AsyncCheckResponse(check_response) => { - print_descriptor("Check Started")?; - stdoutln!( - "Check successfully started with workflow ID: {}", - check_response.workflow_id, - )?; - stdoutln!("View full details at {}", check_response.target_url)?; + Some(format!("Successfully created a new project from the '{}' template in {}/n Read the generated '{}' file for next steps./n{}", + template_id, + path, + readme, + forum_call_to_action)) } + RoverOutput::CheckResponse(check_response) => Some(check_response.get_table()), + RoverOutput::AsyncCheckResponse(check_response) => Some(format!( + "Check successfully started with workflow ID: {}/nView full details at {}", + check_response.workflow_id, check_response.target_url + )), RoverOutput::Profiles(profiles) => { if profiles.is_empty() { stderrln!("No profiles found.")?; - } else { - print_descriptor("Profiles")?; - } - - for profile in profiles { - stdoutln!("{}", profile)?; } + Some(profiles.join("\n")) } RoverOutput::Introspection(introspection_response) => { - print_descriptor("Introspection Response")?; - print_content(introspection_response)?; + Some((introspection_response).to_string()) } RoverOutput::ErrorExplanation(explanation) => { // underline bolded md let mut skin = MadSkin::default(); skin.bold.add_attr(Underlined); - stdoutln!("{}", skin.inline(explanation))?; + Some(format!("{}", skin.inline(explanation))) } RoverOutput::ReadmeFetchResponse { graph_ref: _, content, last_updated_time: _, - } => { - print_descriptor("Readme")?; - print_content(content)?; - } + } => Some((content).to_string()), RoverOutput::ReadmePublishResponse { graph_ref, new_content: _, last_updated_time: _, } => { stderrln!("Readme for {} published successfully", graph_ref,)?; + None } - RoverOutput::EmptySuccess => (), - }; - Ok(()) + RoverOutput::EmptySuccess => None, + }) } pub(crate) fn get_internal_data_json(&self) -> Value { @@ -375,7 +339,7 @@ impl RoverOutput { json!({ "shortlinks": shortlink_vec }) } RoverOutput::FetchResponse(fetch_response) => json!(fetch_response), - RoverOutput::CoreSchema(csdl) => json!({ "core_schema": csdl }), + RoverOutput::SupergraphSchema(csdl) => json!({ "core_schema": csdl }), RoverOutput::CompositionResult(composition_output) => { if let Some(federation_version) = &composition_output.federation_version { json!({ @@ -480,118 +444,46 @@ impl RoverOutput { pub(crate) fn get_json_version(&self) -> JsonVersion { JsonVersion::default() } -} - -fn print_descriptor(descriptor: impl Display) -> io::Result<()> { - if atty::is(Stream::Stdout) { - stderrln!("{}: \n", Style::Heading.paint(descriptor.to_string()))?; - } - Ok(()) -} -fn print_one_line_descriptor(descriptor: impl Display) -> io::Result<()> { - if atty::is(Stream::Stdout) { - stderr!("{}: ", Style::Heading.paint(descriptor.to_string()))?; - } - Ok(()) -} - -/// if the user is outputting to a terminal, we want there to be a terminating -/// newline, but we don't want that newline to leak into output that's piped -/// to a file, like from a `graph fetch` -fn print_content(content: impl Display) -> io::Result<()> { - if atty::is(Stream::Stdout) { - stdoutln!("{}", content) - } else { - stdout!("{}", content) - } -} - -#[derive(Debug, Clone, Serialize)] -pub(crate) struct JsonOutput { - json_version: JsonVersion, - data: JsonData, - error: Value, -} - -impl JsonOutput { - pub(crate) fn success(data: Value, error: Value, json_version: JsonVersion) -> JsonOutput { - JsonOutput { - json_version, - data: JsonData::success(data), - error, - } - } - pub(crate) fn failure(data: Value, error: Value, json_version: JsonVersion) -> JsonOutput { - JsonOutput { - json_version, - data: JsonData::failure(data), - error, + pub(crate) fn print_descriptor(&self) -> io::Result<()> { + if atty::is(Stream::Stdout) { + if let Some(descriptor) = self.descriptor() { + stderrln!("{}: \n", Style::Heading.paint(descriptor))?; + } } + Ok(()) } - - pub(crate) fn print(&self) -> io::Result<()> { - stdoutln!("{}", self) - } -} - -impl fmt::Display for JsonOutput { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", json!(self)) - } -} - -impl From for JsonOutput { - fn from(error: RoverError) -> Self { - let data_json = error.get_internal_data_json(); - let error_json = error.get_internal_error_json(); - JsonOutput::failure(data_json, error_json, error.get_json_version()) - } -} - -impl From for JsonOutput { - fn from(output: RoverOutput) -> Self { - let data = output.get_internal_data_json(); - let error = output.get_internal_error_json(); - JsonOutput::success(data, error, output.get_json_version()) - } -} - -#[derive(Debug, Clone, Serialize)] -pub(crate) struct JsonData { - #[serde(flatten)] - inner: Value, - success: bool, -} - -impl JsonData { - pub(crate) fn success(inner: Value) -> JsonData { - JsonData { - inner, - success: true, + pub(crate) fn print_one_line_descriptor(&self) -> io::Result<()> { + if atty::is(Stream::Stdout) { + if let Some(descriptor) = self.descriptor() { + stderr!("{}: ", Style::Heading.paint(descriptor))?; + } } + Ok(()) } - - pub(crate) fn failure(inner: Value) -> JsonData { - JsonData { - inner, - success: false, + pub(crate) fn descriptor(&self) -> Option<&str> { + match &self { + RoverOutput::ContractDescribe(_) => Some("Configuration Description"), + RoverOutput::ContractPublish(_) => Some("New Configuration Description"), + RoverOutput::FetchResponse(fetch_response) => match fetch_response.sdl.r#type { + SdlType::Graph | SdlType::Subgraph { .. } => Some("Schema"), + SdlType::Supergraph => Some("Supergraph Schema"), + }, + RoverOutput::CompositionResult(_) | RoverOutput::SupergraphSchema(_) => { + Some("Supergraph Schema") + } + RoverOutput::TemplateUseSuccess { .. } => Some("Project generated"), + RoverOutput::CheckResponse(_) => Some("Check Result"), + RoverOutput::AsyncCheckResponse(_) => Some("Check Started"), + RoverOutput::Profiles(_) => Some("Profiles"), + RoverOutput::Introspection(_) => Some("Introspection Response"), + RoverOutput::ReadmeFetchResponse { .. } => Some("Readme"), + RoverOutput::GraphPublishResponse { .. } => Some("Schema Hash"), + _ => None, } } } -#[derive(Debug, Clone, Serialize)] -pub(crate) enum JsonVersion { - #[serde(rename = "1")] - One, -} - -impl Default for JsonVersion { - fn default() -> Self { - JsonVersion::One - } -} - #[cfg(test)] mod tests { use std::collections::BTreeMap; @@ -606,13 +498,15 @@ mod tests { list::{SubgraphInfo, SubgraphUpdatedAt}, }, }, - shared::{ChangeSeverity, SchemaChange, Sdl}, + shared::{ChangeSeverity, SchemaChange, Sdl, SdlType}, }; use apollo_federation_types::build::{BuildError, BuildErrors}; use anyhow::anyhow; + use crate::options::JsonOutput; + use super::*; #[test] @@ -670,7 +564,7 @@ mod tests { #[test] fn core_schema_json() { let mock_core_schema = "core schema contents".to_string(); - let actual_json: JsonOutput = RoverOutput::CoreSchema(mock_core_schema).into(); + let actual_json: JsonOutput = RoverOutput::SupergraphSchema(mock_core_schema).into(); let expected_json = json!( { "json_version": "1", diff --git a/src/command/subgraph/delete.rs b/src/command/subgraph/delete.rs index da9644080..20c8da9c3 100644 --- a/src/command/subgraph/delete.rs +++ b/src/command/subgraph/delete.rs @@ -56,7 +56,7 @@ impl Delete { dry_run, delete_response: delete_dry_run_response, } - .print()?; + .get_stdout()?; // I chose not to error here, since this is a perfectly valid path if !prompt::confirm_delete()? { diff --git a/src/command/subgraph/introspect.rs b/src/command/subgraph/introspect.rs index b7b2e3c6d..b429bd828 100644 --- a/src/command/subgraph/introspect.rs +++ b/src/command/subgraph/introspect.rs @@ -8,7 +8,7 @@ use rover_client::{ operations::subgraph::introspect::{self, SubgraphIntrospectInput}, }; -use crate::options::IntrospectOpts; +use crate::options::{IntrospectOpts, OutputOpts}; use crate::{RoverOutput, RoverResult}; #[derive(Debug, Serialize, Parser)] @@ -18,10 +18,9 @@ pub struct Introspect { } impl Introspect { - pub fn run(&self, client: Client, json: bool) -> RoverResult { + pub fn run(&self, client: Client, output_opts: &OutputOpts) -> RoverResult { if self.opts.watch { - self.exec_and_watch(&client, json)?; - Ok(RoverOutput::EmptySuccess) + self.exec_and_watch(&client, output_opts) } else { let sdl = self.exec(&client, true)?; Ok(RoverOutput::Introspection(sdl)) @@ -42,9 +41,8 @@ impl Introspect { Ok(introspect::run(SubgraphIntrospectInput { headers }, &client, should_retry)?.result) } - pub fn exec_and_watch(&self, client: &Client, json: bool) -> RoverResult { + pub fn exec_and_watch(&self, client: &Client, output_opts: &OutputOpts) -> ! { self.opts - .exec_and_watch(|| self.exec(client, false), json)?; - Ok(RoverOutput::EmptySuccess) + .exec_and_watch(|| self.exec(client, false), output_opts) } } diff --git a/src/command/subgraph/mod.rs b/src/command/subgraph/mod.rs index b73d04c64..b615d3ca7 100644 --- a/src/command/subgraph/mod.rs +++ b/src/command/subgraph/mod.rs @@ -15,6 +15,7 @@ pub use publish::Publish; use clap::Parser; use serde::Serialize; +use crate::options::OutputOpts; use crate::utils::client::StudioClientConfig; use crate::{RoverOutput, RoverResult}; @@ -54,14 +55,16 @@ impl Subgraph { client_config: StudioClientConfig, git_context: GitContext, checks_timeout_seconds: u64, - json: bool, + output_opts: &OutputOpts, ) -> RoverResult { match &self.command { Command::Check(command) => { command.run(client_config, git_context, checks_timeout_seconds) } Command::Delete(command) => command.run(client_config), - Command::Introspect(command) => command.run(client_config.get_reqwest_client()?, json), + Command::Introspect(command) => { + command.run(client_config.get_reqwest_client()?, output_opts) + } Command::Fetch(command) => command.run(client_config), Command::List(command) => command.run(client_config), Command::Publish(command) => command.run(client_config, git_context), diff --git a/src/error/metadata/mod.rs b/src/error/metadata/mod.rs index 3c379e8da..098170559 100644 --- a/src/error/metadata/mod.rs +++ b/src/error/metadata/mod.rs @@ -7,7 +7,7 @@ pub use suggestion::RoverErrorSuggestion; use houston::HoustonProblem; use rover_client::RoverClientError; -use crate::{command::output::JsonVersion, utils::env::RoverEnvKey}; +use crate::{options::JsonVersion, utils::env::RoverEnvKey}; use std::env; diff --git a/src/error/mod.rs b/src/error/mod.rs index 5bd55c653..323676dc6 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -16,10 +16,10 @@ use std::error::Error; use std::fmt::{self, Debug, Display}; use std::io; -use crate::command::output::JsonVersion; - use apollo_federation_types::build::BuildErrors; +use crate::options::JsonVersion; + /// A specialized `Error` type for Rover that wraps `anyhow` /// and provides some extra `Metadata` for end users depending /// on the specific error they encountered. diff --git a/src/options/introspect.rs b/src/options/introspect.rs index 3b586670d..a2fa2446c 100644 --- a/src/options/introspect.rs +++ b/src/options/introspect.rs @@ -2,7 +2,11 @@ use clap::Parser; use reqwest::Url; use serde::{Deserialize, Serialize}; -use crate::{command::output::JsonOutput, utils::parsers::parse_header, RoverOutput, RoverResult}; +use crate::{ + options::{OutputOpts, RoverPrinter}, + utils::parsers::parse_header, + RoverOutput, RoverResult, +}; #[derive(Debug, Serialize, Deserialize, Parser)] pub struct IntrospectOpts { @@ -26,7 +30,7 @@ pub struct IntrospectOpts { } impl IntrospectOpts { - pub fn exec_and_watch(&self, exec_fn: F, json: bool) -> RoverResult + pub fn exec_and_watch(&self, exec_fn: F, output_opts: &OutputOpts) -> ! where F: Fn() -> RoverResult, { @@ -43,11 +47,7 @@ impl IntrospectOpts { if was_updated { let output = RoverOutput::Introspection(sdl.to_string()); - if json { - let _ = JsonOutput::from(output).print(); - } else { - let _ = output.print(); - } + let _ = output.write_or_print(output_opts).map_err(|e| e.print()); } last_result = Some(sdl); } @@ -60,11 +60,7 @@ impl IntrospectOpts { } } if was_updated { - if json { - let _ = JsonOutput::from(error).print(); - } else { - let _ = error.print(); - } + let _ = error.write_or_print(output_opts).map_err(|e| e.print()); } last_result = Some(e); } diff --git a/src/options/mod.rs b/src/options/mod.rs index 3f81a1385..0aa8afbbe 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -3,6 +3,7 @@ mod compose; mod graph; mod introspect; mod license; +mod output; mod profile; mod schema; mod subgraph; @@ -13,6 +14,7 @@ pub(crate) use compose::*; pub(crate) use graph::*; pub(crate) use introspect::*; pub(crate) use license::*; +pub(crate) use output::*; pub(crate) use profile::*; pub(crate) use schema::*; pub(crate) use subgraph::*; diff --git a/src/options/output.rs b/src/options/output.rs new file mode 100644 index 000000000..ccc3bce3f --- /dev/null +++ b/src/options/output.rs @@ -0,0 +1,283 @@ +use std::{fmt, io, str::FromStr}; + +use anyhow::Result; +use calm_io::{stderrln, stdoutln}; +use camino::Utf8PathBuf; +use clap::{error::ErrorKind as ClapErrorKind, CommandFactory, Parser, ValueEnum}; +use rover_std::{Emoji, Fs, Style}; +use serde::Serialize; +use serde_json::{json, Value}; + +use crate::{ + cli::{Rover, RoverOutputFormatKind}, + RoverError, RoverOutput, RoverResult, +}; + +#[derive(Debug, Parser)] +pub struct Output { + /// The file path to write the command output to. + #[clap(long)] + output: OutputOpt, +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +pub enum RoverOutputDestination { + File(Utf8PathBuf), + Stdout, +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +pub enum OutputOpt { + LegacyOutputType(RoverOutputFormatKind), + File(Utf8PathBuf), +} + +impl FromStr for OutputOpt { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if let Ok(format_kind) = RoverOutputFormatKind::from_str(s, true) { + Ok(Self::LegacyOutputType(format_kind)) + } else { + Ok(Self::File(Utf8PathBuf::from(s))) + } + } +} + +pub trait RoverPrinter { + fn write_or_print(self, output_opts: &OutputOpts) -> RoverResult<()>; +} + +impl RoverPrinter for RoverOutput { + fn write_or_print(self, output_opts: &OutputOpts) -> RoverResult<()> { + let (format_kind, output_destination) = output_opts.get_format_and_strategy(); + + // Format the RoverOutput as either plain text or JSON. + let output = match format_kind { + RoverOutputFormatKind::Plain => self.get_stdout(), + RoverOutputFormatKind::Json => Ok(Some(JsonOutput::from(self.clone()).to_string())), + }; + + // Print the RoverOutput to file or stdout. + if let Ok(Some(result)) = output { + match output_destination { + RoverOutputDestination::File(path) => { + let success_heading = Style::Heading.paint(format!( + "{}{} was printed to", + Emoji::Memo, + self.descriptor().unwrap_or("The output") + )); + let path_text = Style::Path.paint(&path); + Fs::write_file(&path, result)?; + stderrln!("{} {}", success_heading, path_text)?; + } + RoverOutputDestination::Stdout => { + // Call the appropriate method based on the variant of RoverOutput. + if let RoverOutput::GraphPublishResponse { .. } = self { + self.print_one_line_descriptor()?; + } else { + self.print_descriptor()?; + } + + stdoutln!("{}", &result)?; + } + } + } + + Ok(()) + } +} + +impl RoverPrinter for RoverError { + fn write_or_print(self, output_opts: &OutputOpts) -> RoverResult<()> { + let (format_kind, output_destination) = output_opts.get_format_and_strategy(); + match format_kind { + RoverOutputFormatKind::Plain => self.print(), + RoverOutputFormatKind::Json => { + let json = JsonOutput::from(self); + match output_destination { + RoverOutputDestination::File(file) => { + let success_heading = Style::Heading + .paint(format!("{}Error JSON was printed to", Emoji::Memo,)); + Fs::write_file(&file, json.to_string())?; + stderrln!("{} {}", success_heading, file)?; + } + RoverOutputDestination::Stdout => json.print()?, + } + Ok(()) + } + }?; + + Ok(()) + } +} + +#[derive(Debug, Parser, Serialize)] +pub struct OutputOpts { + /// Specify Rover's format type + #[arg(long = "format", global = true)] + format_kind: Option, + + /// Specify a file to write Rover's output to + #[arg(long = "output", short = 'o', global = true)] + output_file: Option, +} + +impl OutputOpts { + /// Validates the argument group, exiting early if there are conflicts. + /// This should be called at the start of the application. + pub fn validate_options(&self) { + match (&self.format_kind, &self.output_file) { + (Some(_), Some(OutputOpt::LegacyOutputType(_))) => { + let mut cmd = Rover::command(); + cmd.error( + ClapErrorKind::ArgumentConflict, + "The argument '--output' cannot be used with '--format' when '--output' is not a file", + ) + .exit(); + } + (None, Some(OutputOpt::LegacyOutputType(_))) => { + let warn_prefix = Style::WarningPrefix.paint("WARN:"); + let output_argument = Style::Command.paint("'--output'"); + let format_argument = Style::Command.paint("'--format'"); + eprintln!("{} Future versions of Rover may be incompatible with this usage of {output_argument}.\n\nThe argument for specifying the format of Rover's output has been renamed from {output_argument} to {format_argument}.\nPlease use {format_argument} to configure Rover's output format instead of {output_argument}.\n", warn_prefix); + } + // there are default options, so if nothing is passed, print no errors or warnings + _ => (), + } + } + + /// Handle output and errors from a Rover command. + pub fn handle_output(&self, rover_command_output: T) -> RoverResult<()> + where + T: RoverPrinter, + { + rover_command_output.write_or_print(self) + } + + /// Get the format (plain/json) and strategy (stdout/file) + pub fn get_format_and_strategy(&self) -> (RoverOutputFormatKind, RoverOutputDestination) { + let output_type = self.output_file.clone(); + + match (&self.format_kind, output_type) { + (None, None) + | (None, Some(OutputOpt::LegacyOutputType(RoverOutputFormatKind::Plain))) => { + (RoverOutputFormatKind::Plain, RoverOutputDestination::Stdout) + } + (None, Some(OutputOpt::LegacyOutputType(RoverOutputFormatKind::Json))) => { + (RoverOutputFormatKind::Json, RoverOutputDestination::Stdout) + } + (None, Some(OutputOpt::File(path))) => ( + RoverOutputFormatKind::Plain, + RoverOutputDestination::File(path), + ), + (Some(RoverOutputFormatKind::Plain), None) + | ( + Some(RoverOutputFormatKind::Plain), + Some(OutputOpt::LegacyOutputType(RoverOutputFormatKind::Plain)), + ) => (RoverOutputFormatKind::Plain, RoverOutputDestination::Stdout), + ( + Some(RoverOutputFormatKind::Plain), + Some(OutputOpt::LegacyOutputType(RoverOutputFormatKind::Json)), + ) => (RoverOutputFormatKind::Json, RoverOutputDestination::Stdout), + (Some(RoverOutputFormatKind::Plain), Some(OutputOpt::File(path))) => ( + RoverOutputFormatKind::Plain, + RoverOutputDestination::File(path), + ), + (Some(RoverOutputFormatKind::Json), None) + | (Some(RoverOutputFormatKind::Json), Some(OutputOpt::LegacyOutputType(_))) => { + (RoverOutputFormatKind::Json, RoverOutputDestination::Stdout) + } + (Some(RoverOutputFormatKind::Json), Some(OutputOpt::File(path))) => ( + RoverOutputFormatKind::Json, + RoverOutputDestination::File(path), + ), + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct JsonOutput { + json_version: JsonVersion, + data: JsonData, + error: Value, +} + +impl JsonOutput { + fn success(data: Value, error: Value, json_version: JsonVersion) -> JsonOutput { + JsonOutput { + json_version, + data: JsonData::success(data), + error, + } + } + + fn failure(data: Value, error: Value, json_version: JsonVersion) -> JsonOutput { + JsonOutput { + json_version, + data: JsonData::failure(data), + error, + } + } + + fn print(&self) -> io::Result<()> { + stdoutln!("{}", self) + } +} + +impl fmt::Display for JsonOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", json!(self)) + } +} + +impl From for JsonOutput { + fn from(error: RoverError) -> Self { + let data_json = error.get_internal_data_json(); + let error_json = error.get_internal_error_json(); + JsonOutput::failure(data_json, error_json, error.get_json_version()) + } +} + +impl From for JsonOutput { + fn from(output: RoverOutput) -> Self { + let data = output.get_internal_data_json(); + let error = output.get_internal_error_json(); + JsonOutput::success(data, error, output.get_json_version()) + } +} + +#[derive(Debug, Clone, Serialize)] +struct JsonData { + #[serde(flatten)] + inner: Value, + success: bool, +} + +impl JsonData { + fn success(inner: Value) -> JsonData { + JsonData { + inner, + success: true, + } + } + + fn failure(inner: Value) -> JsonData { + JsonData { + inner, + success: false, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub enum JsonVersion { + #[serde(rename = "1")] + One, +} + +impl Default for JsonVersion { + fn default() -> Self { + JsonVersion::One + } +}