use std::path::Path;
use std::time::Duration;
use serde_json::{Map, Value};
use shep_core::dogs::SHEEP_SCHEMA_KEY;
use super::pane::SheepDogEntry;
use crate::commands::dogs::{DogSchema, ask_schema};
use crate::commands::settings::dog_candidates;
use crate::commands::shep_toml::ShepToml;
pub(super) fn probe_sheep_dogs(
daemon_config: &Path,
home: &Path,
budget: Duration,
) -> Vec<SheepDogEntry> {
let Ok(doc) = ShepToml::read_only(daemon_config) else {
return Vec::new();
};
let candidates = dog_candidates(&doc);
std::thread::scope(|scope| {
let asks: Vec<_> = candidates
.into_iter()
.map(|dog| {
scope.spawn(move || {
let answer = match (crate::dog::builtin_schema(&dog.name), &dog.adopted_path) {
(Some(schema), _) => Some(schema),
(None, Some(path)) => match ask_schema(path, home, &dog.name, budget) {
DogSchema::Published(schema) => Some(schema),
DogSchema::Silent | DogSchema::Unreadable => None,
},
(None, None) => None,
};
SheepDogEntry {
schema: answer.as_ref().and_then(sheep_schema),
name: dog.name,
adopted_path: dog.adopted_path,
}
})
})
.collect();
asks.into_iter().filter_map(|ask| ask.join().ok()).collect()
})
}
pub(crate) fn sheep_schema(root: &Value) -> Option<Value> {
let mut sheep = root.get(SHEEP_SCHEMA_KEY)?.as_object()?.clone();
if let Some(root_defs) = root.get("$defs").and_then(Value::as_object) {
let defs = sheep
.entry("$defs")
.or_insert_with(|| Value::Object(Map::new()));
if let Value::Object(defs) = defs {
for (name, def) in root_defs {
defs.entry(name.clone()).or_insert_with(|| def.clone());
}
}
}
Some(Value::Object(sheep))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn the_sheep_schema_carries_the_roots_defs() {
let root = json!({
"properties": { "poll": { "type": "string" } },
"x-shep-sheep": { "$ref": "#/$defs/ProjectSettings" },
"$defs": { "ProjectSettings": { "type": "object" } },
});
assert_eq!(
sheep_schema(&root),
Some(json!({
"$ref": "#/$defs/ProjectSettings",
"$defs": { "ProjectSettings": { "type": "object" } },
}))
);
}
#[test]
fn a_schema_without_the_key_or_with_a_bare_value_has_none() {
assert_eq!(sheep_schema(&json!({ "properties": {} })), None);
assert_eq!(sheep_schema(&json!({ "x-shep-sheep": true })), None);
}
#[test]
fn the_sheep_schemas_own_definition_wins() {
let root = json!({
"x-shep-sheep": { "$defs": { "Hours": { "type": "string" } } },
"$defs": { "Hours": { "type": "object" }, "Other": {} },
});
let sheep = sheep_schema(&root).expect("a sheep schema");
assert_eq!(sheep["$defs"]["Hours"], json!({ "type": "string" }));
assert_eq!(sheep["$defs"]["Other"], json!({}));
}
#[test]
fn a_shep_toml_that_does_not_parse_lists_no_dog() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("shep.toml");
std::fs::write(&config, "[daemon\n").unwrap();
assert!(probe_sheep_dogs(&config, dir.path(), Duration::from_secs(1)).is_empty());
}
#[cfg(unix)]
#[test]
fn an_adopted_dog_is_asked_and_a_built_in_answers_with_none() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let script = dir.path().join("shep-jobs");
let schema = json!({
"properties": {},
"x-shep-sheep": { "$ref": "#/$defs/Settings" },
"$defs": { "Settings": { "type": "object" } },
});
std::fs::write(
&script,
format!("#!/bin/sh\n[ \"$1\" = --schema ] && echo '{schema}'\nexit 0\n"),
)
.unwrap();
let quiet = dir.path().join("shep-quiet");
std::fs::write(&quiet, "#!/bin/sh\nexit 0\n").unwrap();
let broken = dir.path().join("shep-broken");
std::fs::write(&broken, "#!/bin/sh\necho '{not json'\nexit 0\n").unwrap();
for bin in [&script, &quiet, &broken] {
std::fs::set_permissions(bin, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let config = dir.path().join("shep.toml");
std::fs::write(
&config,
format!(
"[daemon.adopted_dogs]\njobs = {:?}\nquiet = {:?}\nbroken = {:?}\n",
script.display().to_string(),
quiet.display().to_string(),
broken.display().to_string()
),
)
.unwrap();
let mut dogs = probe_sheep_dogs(&config, dir.path(), Duration::from_secs(30));
dogs.sort_by(|a, b| a.name.cmp(&b.name));
let jobs = dogs.iter().find(|dog| dog.name == "jobs").expect("jobs");
assert_eq!(jobs.adopted_path.as_deref(), Some(script.as_path()));
assert_eq!(
jobs.schema,
Some(json!({
"$ref": "#/$defs/Settings",
"$defs": { "Settings": { "type": "object" } },
}))
);
let bark = dogs
.iter()
.find(|dog| dog.name == "bark")
.expect("a built-in");
assert_eq!(bark.schema, None);
for silent in ["quiet", "broken"] {
let dog = dogs.iter().find(|dog| dog.name == silent).expect(silent);
assert_eq!(dog.schema, None, "{silent} answers no schema");
}
}
}