use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use serde::Deserialize;
use shep_client::shep_core::config::AppConfig;
use crate::daemon::Daemon;
use crate::error::Error;
#[derive(Deserialize)]
struct Roll {
apps: Vec<Entry>,
}
#[derive(Deserialize)]
struct Entry {
app: AppConfig,
}
pub async fn registered<D: Daemon>(daemon: &D) -> Result<BTreeMap<String, AppConfig>, Error> {
let path = daemon.save_roll().await?;
let text = fs::read_to_string(&path).map_err(|source| Error::Io {
path: path.clone(),
source,
})?;
read(&path, &text)
}
fn read(path: &Path, text: &str) -> Result<BTreeMap<String, AppConfig>, Error> {
parse(text).map_err(|source| {
Error::Config(format!(
"{}: this shepherd's muster roll is not one this dog can read ({source}). The \
likeliest cause is a newer shepherd than the shep-core this dog was built against: \
the roll carries each sheep's own app config, and an app field added since refuses \
to parse rather than being ignored. Rebuilding or reinstalling shep-deploy against \
the current shep fixes it.",
path.display()
))
})
}
fn parse(text: &str) -> Result<BTreeMap<String, AppConfig>, serde_json::Error> {
let roll: Roll = serde_json::from_str(text)?;
Ok(roll
.apps
.into_iter()
.map(|entry| (entry.app.name.clone(), entry.app))
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn roll_json(apps: &str) -> String {
format!("{{\"version\":1,\"saved_at_ms\":1787756216990,\"apps\":[{apps}]}}")
}
#[test]
fn a_roll_yields_each_sheeps_registered_cwd_and_script() {
let text = roll_json(
"{\"app\":{\"name\":\"bpm\",\"script\":\"bun .\",\"cwd\":\"/srv/reactmap\"},\
\"instances_running\":2}",
);
let apps = parse(&text).expect("parses");
let bpm = apps.get("bpm").expect("bpm is in the roll");
assert_eq!(bpm.cwd.as_deref(), Some("/srv/reactmap"));
assert_eq!(bpm.script, "bun .");
}
#[test]
fn envelope_fields_this_crate_does_not_use_are_ignored() {
let text = "{\"version\":1,\"saved_at_ms\":0,\"something_new\":true,\"apps\":\
[{\"app\":{\"name\":\"web\",\"script\":\"./s\"},\"instances_running\":1,\
\"another_new_one\":[]}]}";
assert!(parse(text).expect("parses").contains_key("web"));
}
#[test]
fn an_empty_flock_is_an_empty_map_not_an_error() {
assert!(parse(&roll_json("")).expect("parses").is_empty());
}
#[test]
fn an_unreadable_roll_names_the_file_and_the_likely_cause() {
let text = "{\"apps\":[{\"app\":{\"name\":\"web\",\"a_field_from_the_future\":1}}]}";
let err = read(Path::new("/srv/shep/flock.json"), text).expect_err("refuses");
let shown = err.to_string();
assert!(shown.contains("/srv/shep/flock.json"), "{shown}");
assert!(shown.contains("newer"), "{shown}");
}
}