use clap::Args as ClapArgs;
use clap::builder::{StringValueParser, TypedValueParser};
use clap::error::{ContextKind, ContextValue, Error as ClapError, ErrorKind};
use clap::{Arg, Command};
use influxdb3_plugin_schemas::ValidationError;
use influxdb3_plugin_schemas::{Index, PluginName};
use influxdb3_plugin_sdk::{SdkError, mutate_index};
use semver::Version;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::str::FromStr;
use crate::cli_error::CliError;
use crate::color::Stream;
use crate::output::error_mapping::{ErrorContext, json_error_from_sdk, json_error_from_validation};
use crate::output::json::{JsonError, YankOutcomeWire, YankOutput, write_envelope_ok};
use crate::output::{Env, OutputMode, RealEnv, resolve_output_mode};
use crate::path_display::{absolutize_for_json, display_relative_to_cwd, paths_overlap};
use crate::style::Palette;
#[derive(Debug, ClapArgs)]
pub(crate) struct Args {
#[arg(value_name = "NAME@VERSION", value_parser = NameAtVersionParser)]
target: NameAtVersion,
#[arg(long, value_enum)]
output: Option<OutputMode>,
#[arg(long)]
index: PathBuf,
#[arg(long)]
out: PathBuf,
#[arg(long)]
undo: bool,
}
impl Args {
pub(crate) fn run(self) -> anyhow::Result<()> {
run_with_env(self, &RealEnv)
}
}
fn run_with_env(args: Args, env: &dyn Env) -> anyhow::Result<()> {
let mode = resolve_output_mode(args.output, env);
let stdout_palette = Palette::for_stream(Stream::Stdout, mode, env, env.stdout_is_terminal());
let index_path = absolutize_for_json(&args.index)?;
let out_path = absolutize_for_json(&args.out)?;
let index_display = index_path.display().to_string();
let out_display = out_path.display().to_string();
let NameAtVersion { name, version } = args.target;
let index_raw = std::fs::read_to_string(&args.index).map_err(|e| {
CliError::runtime(JsonError {
code: "io::read_failed".into(),
message: format!("failed to read --index {index_display}: {e}"),
field: Some(index_display.clone()),
details: Some(serde_json::json!({
"path": index_display,
"io_kind": format!("{:?}", e.kind()),
})),
diagnostics: vec![],
cause: vec![e.to_string()],
})
})?;
let mut index = Index::parse_json(&index_raw).map_err(|schema_errors| {
let diagnostics: Vec<JsonError> = schema_errors
.into_iter()
.map(|reported| json_error_from_validation(&ValidationError::SchemaReported(reported)))
.collect();
CliError::runtime(JsonError {
code: "yank::index_parse_failed".into(),
message: format!("failed to parse --index {index_display} as a registry index"),
field: Some(index_display.clone()),
details: None,
diagnostics,
cause: vec![],
})
})?;
std::fs::create_dir_all(&args.out).map_err(|e| {
CliError::runtime(JsonError {
code: "io::write_failed".into(),
message: format!("failed to create --out {out_display}: {e}"),
field: Some(out_display.clone()),
details: Some(serde_json::json!({
"path": out_display,
"io_kind": format!("{:?}", e.kind()),
})),
diagnostics: vec![],
cause: vec![e.to_string()],
})
})?;
if paths_overlap(&args.index, &args.out, &index_display, &out_display)? {
return Err(CliError::usage(JsonError {
code: "usage::input_output_overlap".into(),
message: format!(
"--out {} resolves to the directory containing --index {}; \
this would overwrite the input index. Use a different --out directory.",
out_display, index_display,
),
field: None,
details: Some(serde_json::json!({
"index": index_display,
"out": out_display,
})),
diagnostics: vec![],
cause: vec![],
}));
}
let sdk_outcome = if args.undo {
mutate_index::unyank(&mut index, name.as_str(), &version)
.map_err(|e| CliError::runtime(json_error_from_sdk(&e, ErrorContext::Yank)))?
} else {
mutate_index::yank(&mut index, name.as_str(), &version)
.map_err(|e| CliError::runtime(json_error_from_sdk(&e, ErrorContext::Yank)))?
};
let derived_index_json = index.to_canonical_json().map_err(|e| {
let sdk_err = SdkError::from(e);
CliError::runtime(json_error_from_sdk(&sdk_err, ErrorContext::Yank))
})?;
let derived_index_path = out_path.join("index.json");
std::fs::write(&derived_index_path, &derived_index_json).map_err(|e| {
CliError::runtime(JsonError {
code: "io::write_failed".into(),
message: format!(
"failed to write derived index {}: {e}",
derived_index_path.display()
),
field: Some(derived_index_path.display().to_string()),
details: Some(serde_json::json!({
"path": derived_index_path.display().to_string(),
"io_kind": format!("{:?}", e.kind()),
})),
diagnostics: vec![],
cause: vec![e.to_string()],
})
})?;
let published_at = index
.plugins
.iter()
.find(|entry| entry.name == name && entry.version == version)
.expect("mutate_index succeeded, so target entry must exist")
.published_at
.to_string();
let payload = YankOutput {
name: name.as_str().to_owned(),
version: version.to_string(),
published_at,
outcome: outcome_wire(sdk_outcome, args.undo),
index_path: derived_index_path,
};
match mode {
OutputMode::Human => {
render_human(&payload, stdout_palette, &mut std::io::stdout())?;
}
OutputMode::Json => {
write_envelope_ok(&mut std::io::stdout(), &payload)?;
}
}
Ok(())
}
fn outcome_wire(outcome: mutate_index::YankOutcome, undo: bool) -> YankOutcomeWire {
match (outcome, undo) {
(mutate_index::YankOutcome::Transitioned, false) => YankOutcomeWire::Yanked,
(mutate_index::YankOutcome::Transitioned, true) => YankOutcomeWire::Unyanked,
(mutate_index::YankOutcome::AlreadyInDesiredState, false) => YankOutcomeWire::AlreadyYanked,
(mutate_index::YankOutcome::AlreadyInDesiredState, true) => {
YankOutcomeWire::AlreadyUnyanked
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct NameAtVersion {
pub(crate) name: PluginName,
pub(crate) version: Version,
}
impl FromStr for NameAtVersion {
type Err = String;
fn from_str(s: &str) -> Result<Self, String> {
let (name_str, ver_str) = s.split_once('@').ok_or_else(|| {
format!(
"expected `<name>@<version>` (e.g., `downsampler@1.2.0`); got {s:?} with no `@` separator"
)
})?;
let name = name_str
.parse::<PluginName>()
.map_err(|e| format!("invalid plugin name {name_str:?}: {e}"))?;
let version = Version::parse(ver_str)
.map_err(|e| format!("invalid SemVer version {ver_str:?}: {e}"))?;
Ok(Self { name, version })
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct NameAtVersionParser;
impl TypedValueParser for NameAtVersionParser {
type Value = NameAtVersion;
fn parse_ref(
&self,
cmd: &Command,
arg: Option<&Arg>,
value: &OsStr,
) -> Result<Self::Value, ClapError> {
let inner = StringValueParser::new();
let s = TypedValueParser::parse_ref(&inner, cmd, arg, value)?;
s.parse::<NameAtVersion>().map_err(|msg| {
let mut err = ClapError::new(ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
ContextKind::InvalidArg,
ContextValue::String(arg.to_string()),
);
}
err.insert(
ContextKind::InvalidValue,
ContextValue::String(format!("{s}: {msg}")),
);
err
})
}
}
fn render_human(
payload: &YankOutput,
palette: Palette,
writer: &mut impl std::io::Write,
) -> std::io::Result<()> {
match payload.outcome {
YankOutcomeWire::Yanked | YankOutcomeWire::Unyanked => {
let yanked = matches!(payload.outcome, YankOutcomeWire::Yanked);
let action = if yanked { "yank" } else { "unyank" };
let warn = palette.warn.render();
let warn_reset = palette.warn.render_reset();
writeln!(
writer,
"{warn}{action}ed {}@{} (yanked={yanked}){warn_reset}",
payload.name, payload.version,
)?;
}
YankOutcomeWire::AlreadyYanked | YankOutcomeWire::AlreadyUnyanked => {
let yanked = matches!(payload.outcome, YankOutcomeWire::AlreadyYanked);
let dim = palette.dim.render();
let dim_reset = palette.dim.render_reset();
writeln!(
writer,
"{dim}{}@{} already in desired state (yanked={yanked}); no change{dim_reset}",
payload.name, payload.version,
)?;
}
}
writeln!(
writer,
" index: {}",
display_relative_to_cwd(&payload.index_path)
)?;
Ok(())
}