use std::io::{Read, Write};
use std::str::FromStr;
pub(crate) mod file;
pub(crate) use file::OutputFile;
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[error(transparent)]
#[remain::sorted]
pub(crate) enum Error {
Io(#[from] std::io::Error),
Json(#[from] serde_json::Error),
Query(#[from] crate::query::Error),
Utf8(#[from] std::string::FromUtf8Error),
Yaml(#[from] serde_yml::Error),
}
#[derive(educe::Educe, Clone, clap::Parser, serde::Deserialize, serde::Serialize)]
#[educe(Debug)]
#[group(id = "output")]
#[remain::sorted]
#[rustfmt::skip]
pub(crate) struct Options {
#[arg(
env = "OUTPUT_DUMP",
long = "output-dump",
id = "output.dump",
value_name = "TYPE",
help_heading = "Output Options",
help = "dump info rather than normal output",
long_help = "dump info rather than normal output.
'config' will output the current config, accounting for given options and environment.
'defaults' will output the program defaults for this command.
'schema' will output a JSON Schema for this command's options.",
)]
pub(crate) dump: Option<Dump>,
#[command(flatten)]
pub(crate) file: OutputFile,
#[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",
long_help = "format to use for output. If omitted, uses plain text.",
)]
pub(crate) format: Format,
#[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: crate::query::Query,
#[arg(
env = "OUTPUT_WRAPPER",
long = "output-wrapper",
id = "output.wrapper",
value_name = "STRING",
help_heading = "Output Options",
help = "name of key for structured output",
long_help = "name of key for structured output.
Both singular and plural forms of the value given will be used internally, and
the final choice of which to use is up to the generator, with the following rules:
The singular form may be either a Scalar or Mapping (YAML) / Object (JSON).
The plural form will always be a Sequence (YAML) / Array (JSON).",
)]
pub(crate) wrapper: Option<Wrapper>,
}
impl crate::Output for Options {}
impl Options {
pub(crate) fn produce<E, F, I, T>(self, default_wrapper: &str, input: I, func: F) -> Result<(), E>
where
E: From<Error>,
F: FnOnce(I, &mut std::collections::BTreeMap<String, Box<dyn crate::Output>>, Wrapper) -> Result<(), E>,
T: Default + crate::Output + schemars::JsonSchema + 'static,
I: Into<T>,
{
let Self {
dump,
file: OutputFile { path },
format,
query,
wrapper,
} = self;
let output = if let Some(dump) = dump {
dump.output::<T, I>(input)
} else {
let wrapper = wrapper.unwrap_or_else(|| Wrapper::new(default_wrapper));
let mut object = std::collections::BTreeMap::new();
func(input, &mut object, wrapper)?;
Box::new(object)
};
let value = serde_json::to_value(output).map_err(Error::from)?;
let value = query.evaluate(&value).map_err(Error::from)?;
let json = serde_json::to_vec(&value).map_err(Error::from)?;
let mut from = serde_json::Deserializer::from_slice(&json);
let mut bytes = Vec::new();
match format {
Format::CompactJson => {
let mut into = serde_json::Serializer::new(&mut bytes);
serde_transcode::transcode(&mut from, &mut into).map_err(Error::from)?;
into.into_inner().flush().map_err(Error::from)?;
}
Format::PrettyJson => {
let mut into = serde_json::Serializer::pretty(&mut bytes);
serde_transcode::transcode(&mut from, &mut into).map_err(Error::from)?;
into.into_inner().flush().map_err(Error::from)?;
}
Format::Yaml => {
let mut into = serde_yml::Serializer::new(&mut bytes);
serde_transcode::transcode(&mut from, &mut into).map_err(Error::from)?;
into.into_inner().map_err(Error::from)?.flush().map_err(Error::from)?;
}
}
let contents = String::from_utf8(bytes).map_err(Error::from)?;
match path.as_ref() {
Some(path) => fs_err::write(path, contents).map_err(Error::from)?,
None => println!("{contents}"),
}
Ok(())
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[remain::sorted]
pub(crate) struct Wrapper {
pub(crate) plural: String,
pub(crate) singular: String,
}
impl Wrapper {
pub(crate) fn new(s: &str) -> Self {
let plural = cruet::to_plural(s);
let singular = cruet::to_singular(s);
Self { plural, singular }
}
}
impl FromStr for Wrapper {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::new(s))
}
}
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum, serde::Deserialize, serde::Serialize)]
#[clap(rename_all = "kebab-case")]
#[remain::sorted]
pub(crate) enum Format {
#[value(alias = "c")]
CompactJson,
#[value(alias = "j")]
PrettyJson,
#[default]
#[value(alias = "y")]
Yaml,
}
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)?),
}
}
}
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum, serde::Deserialize, serde::Serialize)]
pub(crate) enum Dump {
Defaults,
#[default]
Input,
Schema,
}
impl Dump {
pub(crate) fn output<T, I>(self, input: I) -> Box<dyn crate::Output>
where
T: Default + crate::Output + schemars::JsonSchema + 'static,
I: Into<T>,
{
match self {
Self::Defaults => Box::new(T::default()),
Self::Input => Box::new(input.into()),
Self::Schema => Box::new(schemars::schema_for!(T)),
}
}
}