use std::io::IsTerminal as _;
use serde_json::Value as Json;
use shep_client::Client;
use shep_core::paths::ShepPaths;
use shep_core::protocol::{DogSectionToml, Request, Response};
use super::plan::schema_refusal;
use super::{Flags, Plan, Verb, Write, WriteRefusal};
use crate::cli::Format;
use crate::commands::copy;
use crate::commands::copy::address;
use crate::commands::copy::confirm::{self, Answer, Go};
use crate::commands::copy::report::shows_changes_first;
use crate::commands::rpc::{client_error, unexpected_response};
use crate::commands::settings_read::{self, Holder};
use crate::dog_settings::{AskFailure, ask_dog};
use crate::exit::ExitCode;
use crate::output::{CopyChange, SetReport, Streams, emit, write_outcome};
pub(super) async fn run(
client: &Client,
streams: &mut Streams<'_>,
paths: &ShepPaths,
name: &str,
path: &str,
plan: &Plan,
flags: Flags,
) -> ExitCode {
let verb = flags.verb;
let holder = holder_of(&plan.write);
if let Some(refusal) = schema_refusal(plan, name, path) {
return refusal.report(streams);
}
let mut report = report_for(holder, name, path, plan, false, flags.dry_run);
if plan.changes.is_empty() {
let reason = match verb {
Verb::Set => "already holds that value",
Verb::Unset => "holds nothing to remove",
};
announce(
streams,
verb.as_str(),
&format!("nothing to write: `{}.{}` {reason}", name, report.path),
);
return finish(streams, verb, &report);
}
let go = confirm::decide(
flags.dry_run,
flags.yes,
std::io::stdin().is_terminal(),
!plan.changes.is_empty(),
);
if shows_changes_first(streams.fmt, go)
&& let Err(code) = print_changes(streams, verb, &report)
{
return code;
}
if go == Go::Refuse {
return streams.fail(
ExitCode::Usage,
&format!(
"nothing was written: `shep {verb}` asks before it writes, and this run has no \
terminal to ask on. Pass --yes to write without asking, or --dry-run to see the \
changes and write nothing",
verb = verb.as_str()
),
);
}
if go == Go::Ask {
let mut reader = std::io::stdin().lock();
match ask(
streams,
verb,
name,
&report.path,
plan.changes.len(),
&mut reader,
) {
Answer::Yes => {}
Answer::No => {
announce(streams, verb.as_str(), "nothing was written");
return finish(streams, verb, &report);
}
Answer::Failed(reason) => {
return streams.fail(
ExitCode::Failure,
&format!("nothing was written: the question could not be read ({reason})"),
);
}
}
}
if go == Go::DryRun {
announce(streams, "dry_run", "nothing was written");
return finish(streams, verb, &report);
}
if let Some(code) = stale_holder(client, streams, name, &plan.write, verb, &report.path).await {
return code;
}
if let Some((dog, sheep)) = dog_in(&plan.write, name) {
match ask_dog(client, paths, dog, &plan.table, sheep).await {
Ok(()) => {}
Err(AskFailure::Refused { reason }) => {
return WriteRefusal::ByDog(dog_refused(name, dog, &plan.write, &reason))
.report(streams);
}
Err(AskFailure::Unknown { reason }) => {
return WriteRefusal::Refused(format!("nothing was written: {reason}"))
.report(streams);
}
Err(AskFailure::Transport(err)) => return client_error(streams, &err),
}
if let Some(code) =
stale_holder(client, streams, name, &plan.write, verb, &report.path).await
{
return code;
}
}
let pending = match send(client, streams, name, &plan.write).await {
Ok(pending) => pending,
Err(code) => return code,
};
report = report_for(holder, name, path, plan, true, flags.dry_run);
report.pending = pending;
if !report.pending.is_empty() {
announce(
streams,
"parked",
&format!(
"{} waiting on the next spawn (`shep reload {name}` promotes them)",
report.pending.join(", ")
),
);
}
announce(
streams,
"written",
&format!(
"wrote {} change{} onto `{}`",
plan.changes.len(),
if plan.changes.len() == 1 { "" } else { "s" },
name
),
);
finish(streams, verb, &report)
}
fn announce(streams: &mut Streams<'_>, code: &str, message: &str) {
if streams.fmt == Format::Table {
streams.note(code, message);
}
}
pub(super) fn ask(
streams: &mut Streams<'_>,
verb: Verb,
name: &str,
path: &str,
changes: usize,
reader: &mut dyn std::io::BufRead,
) -> Answer {
let question = if changes == 1 {
format!("{} `{}.{}`? [y/N] ", verb.as_str(), name, path)
} else {
format!(
"{} {changes} keys under `{}.{}`? [y/N] ",
verb.as_str(),
name,
path
)
};
streams.aside("confirm", &question);
confirm::read_answer(reader)
}
async fn send(
client: &Client,
streams: &mut Streams<'_>,
name: &str,
write: &Write,
) -> Result<Vec<String>, ExitCode> {
let request = match write {
Write::SheepField { key, value } => Request::SetSheepField {
name: name.to_string(),
key: key.clone(),
value: value.clone(),
},
Write::SheepDogTable { dog, table, .. } => Request::SetSheepDogSettings {
name: name.to_string(),
dog: dog.clone(),
table: Some(table.clone()),
},
Write::DropSheepDogTable { dog, .. } => Request::SetSheepDogSettings {
name: name.to_string(),
dog: dog.clone(),
table: None,
},
Write::ResetSheepField { key } => Request::ResetSheepField {
name: name.to_string(),
key: key.clone(),
},
Write::DogSection { toml, .. } => Request::SetDogConfig {
name: name.to_string(),
toml: DogSectionToml::from(toml.clone()),
},
};
let pending_key = match write {
Write::SheepField { key, .. } | Write::ResetSheepField { key } => key.clone(),
_ => String::new(),
};
match client.request(request).await {
Ok(Response::SheepFieldSet {
pending, warning, ..
}) => {
if let Some(warning) = warning {
streams.aside("warning", &warning);
}
Ok(pending.then_some(pending_key).into_iter().collect())
}
Ok(Response::SheepFieldReset {
key,
value,
pending,
warning,
..
}) => {
if let Some(warning) = warning {
streams.aside("warning", &warning);
}
announce(
streams,
"reset",
&format!("`{name}.{key}` holds {value} again"),
);
Ok(pending.then_some(pending_key).into_iter().collect())
}
Ok(Response::SheepDogSettingsSet { .. }) | Ok(Response::DogConfigSet { .. }) => {
Ok(Vec::new())
}
Ok(_other) => Err(unexpected_response(streams)),
Err(err) => Err(client_error(streams, &err)),
}
}
async fn stale_holder(
client: &Client,
streams: &mut Streams<'_>,
name: &str,
write: &Write,
verb: Verb,
path: &str,
) -> Option<ExitCode> {
match holder_unchanged(client, streams, name, write).await {
Ok(true) => None,
Ok(false) => {
Some(WriteRefusal::Stale(holder_moved(verb, name, path, write)).report(streams))
}
Err(code) => Some(code),
}
}
pub(super) async fn holder_unchanged(
client: &Client,
streams: &mut Streams<'_>,
name: &str,
write: &Write,
) -> Result<bool, ExitCode> {
Ok(match write {
Write::SheepField { .. } | Write::ResetSheepField { .. } => true,
Write::DogSection { before, .. } => {
match settings_read::read_dog_section(client, streams, name).await {
Ok(now) => !section_moved(before, &now),
Err(code) => return Err(code),
}
}
Write::SheepDogTable { dog, before, .. } | Write::DropSheepDogTable { dog, before } => {
let config = match copy::read_config(client, streams, name).await {
Ok(config) => config,
Err(code) => return Err(code),
};
let now = address::dog_table(&config, dog)
.and_then(Json::as_object)
.cloned()
.unwrap_or_default();
!table_moved(before, &Json::Object(now))
}
})
}
pub(super) fn section_moved(before: &str, now: &str) -> bool {
before.trim() != now.trim()
}
pub(super) fn table_moved(before: &Json, now: &Json) -> bool {
before != now
}
pub(super) fn holder_moved(verb: Verb, name: &str, path: &str, write: &Write) -> String {
let address = if path.is_empty() {
name.to_string()
} else {
format!("{name}.{path}")
};
let verb = verb.as_str();
format!(
"nothing was written: {} changed while `shep {verb}` waited for the answer. `shep \
get {address}` reads what it holds now, and `shep {verb}` again plans against that",
holder_phrase(name, write),
)
}
fn holder_of(write: &Write) -> Holder {
match write {
Write::DogSection { .. } => Holder::Dog,
_other => Holder::Sheep,
}
}
fn dog_in<'a>(write: &'a Write, name: &'a str) -> Option<(&'a str, Option<&'a str>)> {
match write {
Write::SheepDogTable { dog, .. } | Write::DropSheepDogTable { dog, .. } => {
Some((dog.as_str(), Some(name)))
}
Write::DogSection { .. } => Some((name, None)),
Write::SheepField { .. } | Write::ResetSheepField { .. } => None,
}
}
fn holder_phrase(name: &str, write: &Write) -> String {
match write {
Write::DogSection { .. } => format!("`{name}`'s section"),
Write::SheepDogTable { dog, .. } | Write::DropSheepDogTable { dog, .. } => {
format!("`{name}`'s table for `{dog}`")
}
Write::SheepField { .. } | Write::ResetSheepField { .. } => format!("`{name}`'s field"),
}
}
fn dog_refused(name: &str, dog: &str, write: &Write, reason: &str) -> String {
format!(
"nothing was written: `{dog}` refused {}: {reason}",
holder_phrase(name, write)
)
}
pub(super) fn report_for(
holder: Holder,
name: &str,
path: &str,
plan: &Plan,
written: bool,
dry_run: bool,
) -> SetReport {
SetReport {
kind: holder.as_str(),
name: name.to_string(),
path: path.to_string(),
written,
dry_run,
pending: Vec::new(),
changes: plan
.changes
.iter()
.map(|change| CopyChange {
key: change.key.clone(),
change: change.kind.as_str(),
old: change.old.clone(),
new: change.new.clone(),
})
.collect(),
}
}
fn print_changes(
streams: &mut Streams<'_>,
verb: Verb,
report: &SetReport,
) -> Result<(), ExitCode> {
let writer: &mut dyn std::io::Write = if streams.fmt == Format::Table {
&mut *streams.out
} else {
&mut *streams.err
};
emit(
writer,
Format::Table,
verb.as_str(),
report.clone(),
streams.style,
)
.map_err(|_| ExitCode::Failure)
}
fn finish(streams: &mut Streams<'_>, verb: Verb, report: &SetReport) -> ExitCode {
if streams.fmt != Format::Json {
return ExitCode::Success;
}
write_outcome(emit(
&mut *streams.out,
Format::Json,
verb.as_str(),
report.clone(),
streams.style,
))
}