use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Verify {
#[default]
Probed,
Alive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Watch {
#[default]
Auto,
Manual,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct State {
pub remote: String,
pub branch: String,
pub deployed: Option<String>,
#[serde(default)]
pub failed: Option<String>,
#[serde(default)]
pub verify: Verify,
#[serde(default)]
pub watch: Watch,
pub origin_cwd: Option<PathBuf>,
pub origin_script: Option<String>,
pub checkout: PathBuf,
}
impl State {
pub fn read(path: &Path) -> Result<Self, Error> {
let text = fs::read_to_string(path).map_err(|source| Error::Io {
path: path.to_owned(),
source,
})?;
toml::from_str(&text)
.map_err(|source| Error::Config(format!("{}: {source}", path.display())))
}
pub fn write(&self, path: &Path) -> Result<(), Error> {
let text = toml::to_string_pretty(self).map_err(|source| Error::Io {
path: path.to_owned(),
source: io::Error::other(source),
})?;
let mut tmp = path.as_os_str().to_owned();
tmp.push(".tmp");
let tmp = PathBuf::from(tmp);
fs::write(&tmp, text).map_err(|source| Error::Io {
path: tmp.clone(),
source,
})?;
fs::rename(&tmp, path).map_err(|source| Error::Io {
path: path.to_owned(),
source,
})
}
}
#[cfg(test)]
mod tests {
#[test]
fn an_unknown_key_in_the_record_is_refused_and_named() {
let toml = r#"
remote = "https://example.invalid/repo.git"
branch = "main"
checkout = "/srv/web"
verfiy = "alive"
"#;
let err = toml::from_str::<State>(toml).expect_err("a typo must not parse");
assert!(
format!("{err}").contains("verfiy"),
"the refusal must name the key an operator typed: {err}"
);
}
use super::*;
#[test]
fn state_round_trips_through_toml() {
let original = State {
remote: "https://github.com/WatWowMap/ReactMap".into(),
branch: "main".into(),
deployed: Some("a1b2c3d".into()),
failed: None,
verify: Verify::Probed,
watch: Watch::Manual,
origin_cwd: Some(PathBuf::from("/srv/reactmap")),
origin_script: Some("bun .".into()),
checkout: PathBuf::from("/srv/reactmap"),
};
let text = toml::to_string(&original).expect("serialises");
let back: State = toml::from_str(&text).expect("parses");
assert_eq!(back, original);
}
#[test]
fn reading_a_missing_state_file_names_it() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("deploy.toml");
let err = State::read(&path).expect_err("nothing to read");
assert!(err.to_string().contains("deploy.toml"));
}
#[test]
fn an_absent_watch_defaults_to_auto() {
let text = r#"
remote = "https://example.com/x"
branch = "main"
checkout = "/srv/x"
"#;
let state: State = toml::from_str(text).expect("parses");
assert_eq!(state.watch, Watch::Auto);
}
#[test]
fn an_absent_verify_defaults_to_probed() {
let text = r#"
remote = "https://example.com/x"
branch = "main"
checkout = "/srv/x"
"#;
let state: State = toml::from_str(text).expect("parses");
assert_eq!(state.verify, Verify::Probed);
}
fn sample(deployed: Option<&str>) -> State {
State {
remote: "https://example.com/x".into(),
branch: "main".into(),
deployed: deployed.map(str::to_owned),
failed: None,
verify: Verify::default(),
watch: Watch::default(),
origin_cwd: None,
origin_script: None,
checkout: PathBuf::from("/srv/x"),
}
}
#[test]
fn write_survives_a_second_write_and_leaves_no_tmp_file_behind() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("deploy.toml");
let tmp = dir.path().join("deploy.toml.tmp");
let first = sample(None);
first.write(&path).expect("first write");
assert!(path.exists(), "the state file must exist after a write");
assert!(
!tmp.exists(),
"a completed write must not leave a .tmp file"
);
let second = sample(Some("a1b2c3d"));
second.write(&path).expect("second write");
assert!(
!tmp.exists(),
"a second, overwriting write must also leave no .tmp file"
);
assert_eq!(State::read(&path).expect("reads"), second);
}
}