use serde_json::{Map, Value as Json};
use shep_client::Client;
use shep_client::RequestError;
use shep_core::dogs::validate::{Proposed, VALIDATE_ACTION, Verdict};
use shep_core::paths::ShepPaths;
use shep_core::protocol::{ActionOutcome, Request, Response, RpcError, RpcErrorCode, SelectorSpec};
use crate::commands::dogs::{ManifestRead, VERSION_BUDGET, read_manifest};
use crate::commands::rpc::deadline_for;
use crate::commands::settings_write::value::table_json;
use crate::commands::shep_toml::ShepToml;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum AskFailure {
Refused {
reason: String,
},
Unknown {
reason: String,
},
Transport(RequestError),
}
impl AskFailure {
pub(crate) fn request_error(&self) -> RequestError {
match self {
Self::Refused { reason } | Self::Unknown { reason } => RequestError::Rpc(RpcError {
code: RpcErrorCode::InvalidConfig,
message: reason.clone(),
daemon_version: None,
}),
Self::Transport(err) => err.clone(),
}
}
}
pub(crate) async fn ask_dog(
client: &Client,
paths: &ShepPaths,
dog: &str,
table: &Map<String, Json>,
sheep: Option<&str>,
) -> Result<(), AskFailure> {
match declares_validate(dog, paths) {
Declaration::Silent => return Ok(()),
Declaration::Declared => {}
Declaration::Unknown { reason } => return Err(AskFailure::Unknown { reason }),
}
let proposed = Proposed::new(table.clone(), sheep.map(str::to_owned));
let ask = Request::Trigger {
selector: SelectorSpec::Name(dog.to_string()),
action: VALIDATE_ACTION.to_string(),
params: Some(proposed.params()),
};
let deadline = deadline_for(&ask);
match client.request_with_deadline(ask, deadline).await {
Ok(Response::Triggered(rows)) => {
if let Some(failure) = rows.iter().find_map(|row| refusal_for(dog, &row.outcome)) {
return Err(failure);
}
if rows.is_empty() {
return Err(AskFailure::Unknown {
reason: format!(
"`{dog}` was asked to validate the write and the shepherd answered \
with no row for it",
),
});
}
Ok(())
}
Err(RequestError::Rpc(err)) if err.code == RpcErrorCode::NotFound => Ok(()),
Err(err) => Err(AskFailure::Transport(err)),
Ok(other) => Err(AskFailure::Transport(RequestError::UnexpectedReply {
asked: "Trigger",
answered: other.name(),
})),
}
}
fn refusal_for(dog: &str, outcome: &ActionOutcome) -> Option<AskFailure> {
match outcome {
ActionOutcome::Replied { body } => match Verdict::read(body) {
Verdict::Accept => None,
Verdict::Refuse { reason } => Some(AskFailure::Refused { reason }),
},
ActionOutcome::TimedOut => Some(AskFailure::Refused {
reason: format!(
"`{dog}` was asked to validate the write and answered nothing within its \
`action_timeout`"
),
}),
ActionOutcome::NoChannel | ActionOutcome::DogNoChannel | ActionOutcome::Skipped => None,
_other => Some(AskFailure::Unknown {
reason: format!(
"`{dog}` answered the ask with something this shep cannot read: {_other:?}",
),
}),
}
}
pub(crate) fn dog_write(request: &Request) -> Option<DogWrite<'_>> {
match request {
Request::SetSheepDogSettings {
name, dog, table, ..
} => Some(DogWrite {
dog,
sheep: Some(name),
table: table
.as_ref()
.map_or_else(Map::new, |table| table.as_map().clone()),
}),
Request::SetDogConfig { name, toml } => Some(DogWrite {
dog: name,
sheep: None,
table: section_table(toml.as_str())?,
}),
_other => None,
}
}
pub(crate) struct DogWrite<'a> {
pub dog: &'a str,
pub sheep: Option<&'a str>,
pub table: Map<String, Json>,
}
pub(crate) fn section_table(toml: &str) -> Option<Map<String, Json>> {
Some(table_json(
toml.parse::<toml_edit::DocumentMut>().ok()?.as_table(),
))
}
enum Declaration {
Declared,
Silent,
Unknown {
reason: String,
},
}
fn declares_validate(dog: &str, paths: &ShepPaths) -> Declaration {
let doc = match ShepToml::read_only(&paths.daemon_config) {
Ok(doc) => doc,
Err(err) => {
return Declaration::Unknown {
reason: format!(
"shep cannot tell whether `{dog}` wants to be consulted before this write: \
{} is unreadable ({err})",
paths.daemon_config.display()
),
};
}
};
let Some(path) = doc.adopted_dog_path(dog) else {
return Declaration::Silent;
};
match read_manifest(&path, &paths.home, dog, VERSION_BUDGET) {
Ok(ManifestRead::Read(manifest)) => {
if manifest.validate {
Declaration::Declared
} else {
Declaration::Silent
}
}
Ok(ManifestRead::Absent) => Declaration::Silent,
Ok(ManifestRead::Unreadable) => Declaration::Unknown {
reason: format!(
"shep cannot tell whether `{dog}` wants to be consulted before this write: the \
manifest cached for {} cannot be read, and the binary answered nothing when \
asked again",
path.display()
),
},
Err(err) => Declaration::Unknown {
reason: format!(
"shep cannot tell whether `{dog}` wants to be consulted before this write: its \
manifest at {} could not be read ({err})",
path.display()
),
},
}
}