use std::collections::BTreeSet;
use std::fmt;
use serde_json::{Map, Value as Json};
use shep_client::Client;
use shep_core::config::DogTable;
use shep_core::paths::ShepPaths;
#[cfg(doc)]
use shep_core::protocol::Request;
use toml_edit::DocumentMut;
use self::plan::{field_changes, plan_dog_table, plan_section};
use self::schema::{dog_schema, known_dogs};
use self::value::value_of;
use crate::cli::SetArgs;
use crate::commands::copy;
use crate::commands::copy::CopyRefusal;
use crate::commands::copy::address::{self, ConfigPath, FlockNames};
use crate::commands::copy::plan::Change;
use crate::commands::settings_read;
use crate::exit::ExitCode;
use crate::lookout::probe_sheep_dogs::sheep_schema;
use crate::output::Streams;
mod edit;
mod flow;
#[cfg(test)]
mod flow_stale_tests;
mod plan;
mod schema;
mod unset;
#[cfg(test)]
mod unset_plan_tests;
pub(crate) mod value;
use self::flow::run;
pub(crate) use self::unset::unset;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Verb {
Set,
Unset,
}
impl Verb {
const fn as_str(self) -> &'static str {
match self {
Self::Set => "set",
Self::Unset => "unset",
}
}
}
#[derive(Debug, Clone, Copy)]
struct Flags {
verb: Verb,
yes: bool,
dry_run: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum WriteRefusal {
Usage(String),
Unknown(String),
Refused(String),
Invalid(String),
ByDog(String),
Stale(String),
}
impl WriteRefusal {
fn message(&self) -> &str {
match self {
Self::Usage(message)
| Self::Unknown(message)
| Self::Refused(message)
| Self::Invalid(message)
| Self::ByDog(message)
| Self::Stale(message) => message,
}
}
const fn code(&self) -> ExitCode {
match self {
Self::Usage(_) => ExitCode::Usage,
Self::Unknown(_) => ExitCode::NotFound,
Self::Refused(_) | Self::Invalid(_) | Self::ByDog(_) => ExitCode::InvalidConfig,
Self::Stale(_) => ExitCode::Failure,
}
}
fn report(&self, streams: &mut Streams<'_>) -> ExitCode {
streams.fail(self.code(), self.message())
}
}
impl fmt::Display for WriteRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.message())
}
}
impl core::error::Error for WriteRefusal {}
#[derive(Debug, Clone)]
pub(super) enum Write {
SheepField {
key: String,
value: Json,
},
SheepDogTable {
dog: String,
table: DogTable,
before: Json,
},
DropSheepDogTable {
dog: String,
before: Json,
},
ResetSheepField {
key: String,
},
DogSection {
toml: String,
before: String,
},
}
#[derive(Debug)]
pub(super) struct Plan {
changes: Vec<Change>,
write: Write,
table: Map<String, Json>,
schema: Option<Json>,
touched: BTreeSet<Vec<String>>,
}
pub(super) struct Ask<'a> {
flags: Flags,
value: Json,
flock: &'a FlockNames,
paths: &'a ShepPaths,
}
pub(crate) async fn set(
client: &Client,
streams: &mut Streams<'_>,
paths: &ShepPaths,
args: &SetArgs,
) -> ExitCode {
let value = match value_of(&args.value) {
Ok(value) => value,
Err(refusal) => return refusal.report(streams),
};
let flock = match copy::flock_names(client, streams).await {
Ok(names) => names,
Err(code) => return code,
};
let segments = match settings_read::segments(&args.address) {
Ok(segments) => segments,
Err(refusal) => return refusal.report(streams),
};
let ask = Ask {
flags: Flags {
verb: Verb::Set,
yes: args.yes,
dry_run: args.dry_run,
},
value,
flock: &flock,
paths,
};
if let Some(name) = address::longest_in(&segments, &flock.sheep) {
return set_sheep(client, streams, &name, &segments, &ask).await;
}
set_dog(client, streams, &segments, &ask).await
}
async fn set_sheep(
client: &Client,
streams: &mut Streams<'_>,
name: &str,
segments: &[String],
ask: &Ask<'_>,
) -> ExitCode {
let config = match copy::read_config(client, streams, name).await {
Ok(config) => config,
Err(code) => return code,
};
let path = match path_in(name, segments, ask.flock, &config) {
Ok(path) => path,
Err(refusal) => return refusal.report(streams),
};
let plan = match &path {
ConfigPath::Field { name: field } => Plan {
changes: field_changes(field, &config, &ask.value),
write: Write::SheepField {
key: field.clone(),
value: ask.value.clone(),
},
table: Map::new(),
schema: None,
touched: BTreeSet::new(),
},
ConfigPath::Env => {
return CopyRefusal::Refused(address::ENV_NOT_WRITTEN.to_string()).report(streams);
}
ConfigPath::Dog { dog, keys } => {
let plan = match plan_dog_table(name, dog, keys, &config, &ask.value) {
Ok(plan) => plan,
Err(refusal) => return refusal.report(streams),
};
let schema = dog_schema(dog, ask.paths).and_then(|root| sheep_schema(&root));
Plan { schema, ..plan }
}
};
run(
client,
streams,
ask.paths,
name,
&path.dotted(),
&plan,
ask.flags,
)
.await
}
fn path_in(
name: &str,
segments: &[String],
flock: &FlockNames,
config: &Json,
) -> Result<ConfigPath, CopyRefusal> {
let depth = name.split('.').count();
let written = address::Written {
sheep: name.to_string(),
segments: segments[depth..].to_vec(),
};
let dogs = address::DogNames::new(flock, config, &Json::Null);
address::path_of(&written, &dogs, config, config)
}
pub(super) struct DogTarget {
name: String,
section: Option<String>,
}
impl DogTarget {
fn document(&self, streams: &mut Streams<'_>) -> Result<DocumentMut, ExitCode> {
let Some(section) = &self.section else {
return Ok(DocumentMut::new());
};
section
.parse::<DocumentMut>()
.map_err(|_| streams.fail(ExitCode::InvalidConfig, &unparsable(&self.name)))
}
fn rest<'a>(&self, segments: &'a [String]) -> &'a [String] {
&segments[self.name.split('.').count()..]
}
}
async fn set_dog(
client: &Client,
streams: &mut Streams<'_>,
segments: &[String],
ask: &Ask<'_>,
) -> ExitCode {
let target = match resolve_dog(client, streams, segments, ask.flock, ask.paths).await {
Ok(target) => target,
Err(code) => return code,
};
let doc = match target.document(streams) {
Ok(doc) => doc,
Err(code) => return code,
};
let rest = target.rest(segments);
let keys = settings_read::dog_steps(doc.as_table(), rest);
match plan_section(doc, &target.name, &keys, &ask.value) {
Ok(plan) => {
let plan = Plan {
schema: dog_schema(&target.name, ask.paths),
..plan
};
run(
client,
streams,
ask.paths,
&target.name,
&rest.join("."),
&plan,
ask.flags,
)
.await
}
Err(refusal) => refusal.report(streams),
}
}
pub(super) async fn resolve_dog(
client: &Client,
streams: &mut Streams<'_>,
segments: &[String],
flock: &FlockNames,
paths: &ShepPaths,
) -> Result<DogTarget, ExitCode> {
let mut named: Option<String> = None;
let mut sectioned: Option<String> = None;
let known = known_dogs(paths);
for name in settings_read::dog_candidates(segments) {
let section = settings_read::read_dog_section(client, streams, &name).await?;
let holds = flock.dogs.contains(&name) || known.contains(&name);
if holds && named.is_none() {
named = Some(name.clone());
}
if section.trim().is_empty() {
continue;
}
sectioned = Some(name.clone());
if !holds {
continue;
}
return Ok(DogTarget {
name,
section: Some(section),
});
}
match (named, sectioned) {
(Some(name), _) => Ok(DogTarget {
name,
section: None,
}),
(None, Some(name)) => Err(WriteRefusal::Unknown(no_known_dog(&name)).report(streams)),
(None, None) => Err(WriteRefusal::Unknown(no_such_name(segments)).report(streams)),
}
}
pub(super) fn not_a_number(path: &str) -> String {
let under = if path.is_empty() {
String::new()
} else {
format!(" under `{path}`")
};
format!(
"not a value `shep set` writes: TOML's `nan` and `inf`{under} have no place in the JSON \
the shepherd carries, and the flock would be sent a null. Quote the text (`\"nan\"`) to \
write it as a string"
)
}
pub(super) fn no_such_name(segments: &[String]) -> String {
format!(
"no sheep or dog named `{}`: `shep flock` lists the flock and `shep dogs` lists the dogs. \
The key/value store `shep set` used to write lives at `shep kv set`",
segments.join(".")
)
}
pub(super) fn no_known_dog(name: &str) -> String {
format!(
"nothing was written: `dogs.toml` has a `[{name}]` section, but no dog this shepherd knows \
is named `{name}`. `shep adopt` or `shep enable` makes one known, and `shep dogs` lists \
what is"
)
}
pub(super) fn unparsable(name: &str) -> String {
format!(
"`{name}`'s section of `dogs.toml` does not parse: nothing was written, and `shep get \
{name}` reads the same text"
)
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod value_tests;