use crate::{
input::{self, InputData},
patch::{self, Patch},
query::{self, Query},
};
use fred::prelude::{Client, ClientLike, EventInterface, KeysInterface};
use serde_json::Value as JsonValue;
use serde_yml::Value as YamlValue;
use tap::Tap;
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[error(transparent)]
#[remain::sorted]
pub(crate) enum Error {
#[error("(╯°□°)╯︵ ┻━┻ No. Absolutely not. STDIN is not for output data/patches. 😒")]
AbsolutelyNot,
Data(#[from] input::data::Error),
Integration(#[from] crate::integrations::fred::Error),
Io(#[from] std::io::Error),
Json(#[from] serde_json::Error),
Patch(#[from] patch::Error),
Query(#[from] query::Error),
Valkey(#[from] fred::error::Error),
Yaml(#[from] serde_yml::Error),
}
const OUTPUT_DATA_HELP: &str = "value to use for output";
const OUTPUT_DATA_LONG_HELP: &str = constcat::concat!(OUTPUT_DATA_HELP, ".\n", input::INPUT_SOURCE_LONG_HELP, "
This data will be used IN PLACE of whatever output this command would have generated. You can use this to customize the output.");
const OUTPUT_PATCH_HELP: &str = "patch to modify output data";
const OUTPUT_PATCH_LONG_HELP: &str = constcat::concat!(
OUTPUT_PATCH_HELP,
".\n",
input::INPUT_SOURCE_LONG_HELP,
patch::LONG_HELP
);
#[derive(educe::Educe, Clone, clap::Args)]
#[educe(Debug)]
#[group(id = "output")]
#[remain::sorted]
#[rustfmt::skip]
pub(crate) struct Output {
#[arg(
env = "OUTPUT_DATA",
long = "output-data",
id = "output.data",
help_heading = "Output Options",
help = OUTPUT_DATA_HELP,
long_help = OUTPUT_DATA_LONG_HELP,
)]
pub(crate) data: Option<InputData>,
#[arg(
env = "OUTPUT_FILE",
long = "output-file",
id = "output.file",
value_name = "PATH",
help_heading = "Output Options",
help = "write output to file"
)]
pub(crate) file: Option<camino::Utf8PathBuf>,
#[arg(
env = "OUTPUT_FORMAT",
long = "output-format",
id = "output.format",
value_name = "FORMAT",
default_value = "yaml",
help_heading = "Output Options",
help = "format to use for output",
)]
pub(crate) format: Format,
#[arg(
env = "OUTPUT_PATCH",
long = "output-patch",
id = "output.patch",
value_name = "SOURCE",
default_value = "[]",
help_heading = "Output Options",
help = OUTPUT_PATCH_HELP,
long_help = OUTPUT_PATCH_LONG_HELP,
)]
pub(crate) patch: InputData,
#[arg(
env = "OUTPUT_QUERY",
long = "output-query",
id = "output.query",
value_name = "EXPRESSION",
default_value = "",
help_heading = "Output Options",
help = "expression for navigating output",
long_help = "expression for navigating output.
Accepts both RFC6901 (JSON Pointer) and RFC9535 (JSONPath) syntax.",
)]
#[educe(Debug(ignore))]
pub(crate) query: Query,
#[arg(
env = "OUTPUT_STDOUT",
long = "output-stdout",
id = "output.stdout",
action = clap::ArgAction::Set,
help_heading = "Output Options",
help = "write output to STDOUT",
long_help = "write output to STDOUT.
By default, writes to STDOUT unless another output target was given.",
)]
pub(crate) stdout: Option<bool>,
#[arg(
env = "OUTPUT_VALKEY",
long = "output-valkey",
id = "output.valkey",
value_name = "KEY",
help_heading = "Output Options",
help = "write output to Valkey DB at this key",
)]
pub(crate) valkey: Option<String>,
}
impl Output {
#[allow(dependency_on_unit_never_type_fallback)]
pub(crate) async fn produce(
self,
value: JsonValue,
valkey_options: &crate::integrations::fred::ValkeyOptions,
) -> Result<(), Error> {
if false
|| matches!(&self, Self { data: Some(data), .. } if data.source.is_stdin())
|| matches!(&self, Self { patch, .. } if patch.source.is_stdin())
{
return Err(Error::AbsolutelyNot);
}
let Self {
data,
file,
format,
patch,
query,
stdout,
valkey,
} = self;
let value = match data {
Some(data) => data.consume()?,
None => value,
};
let value = {
let mut value = query.evaluate(&value)?.to_owned();
let patch: crate::patch::Patch = patch.consume()?;
patch.apply(&mut value)?;
value
};
let contents = format.serialize(&value)?;
if stdout.unwrap_or_else(|| file.is_none() && valkey.is_none()) {
println!("{contents}");
}
if let Some(path) = file {
if let Some(path) = path.parent() {
fs_err::create_dir_all(path)?
}
fs_err::write(path, &contents)?;
}
if let Some(key) = valkey {
let client = Client::try_from(valkey_options.to_owned())?;
client.init().await?;
client.set(key, contents.trim(), None, None, false).await?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum, serde::Deserialize, serde::Serialize)]
#[clap(rename_all = "kebab-case")]
#[remain::sorted]
pub(crate) enum Format {
#[value(aliases = ["c", "compact"])]
CompactJson,
#[value(aliases = ["j", "json"])]
PrettyJson,
#[default]
#[value(aliases = ["y", "yml"])]
Yaml,
#[value(aliases = ["yd", "yamld", "yaml-doc"])]
YamlDocument,
}
impl Format {
pub(crate) fn serialize<T>(&self, value: &T) -> Result<String, Error>
where
T: serde::Serialize,
{
match self {
Self::CompactJson => Ok(serde_json::to_string(value)?),
Self::PrettyJson => Ok(serde_json::to_string_pretty(value)?),
Self::Yaml => Ok(serde_yml::to_string(value)?),
Self::YamlDocument => Ok(serde_yml::to_string(value)?.tap_mut(|s| s.insert_str(0, "---\n"))),
}
}
}