use std::path::PathBuf;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::paths::AppPaths;
use crate::service::{ServiceError, read_state_record, remove_state_record, write_state_record};
pub const CONFIG_FILE: &str = "coordinator.toml";
pub const LINK_FILE: &str = "coordinator-link.toml";
const CONFIG_SCHEMA_VERSION: u32 = 1;
const LINK_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CoordinatorConfig {
pub schema_version: u32,
pub url: String,
pub host_id: String,
pub name: String,
pub joined_at: DateTime<Utc>,
}
impl CoordinatorConfig {
#[must_use]
pub const fn new(url: String, host_id: String, name: String, joined_at: DateTime<Utc>) -> Self {
Self {
schema_version: CONFIG_SCHEMA_VERSION,
url,
host_id,
name,
joined_at,
}
}
}
fn config_path(paths: &AppPaths) -> PathBuf {
paths.config_dir().join(CONFIG_FILE)
}
fn link_path(paths: &AppPaths) -> PathBuf {
paths.state_dir().join(LINK_FILE)
}
pub fn configured(paths: &AppPaths) -> Result<Option<CoordinatorConfig>, ServiceError> {
read_state_record(&config_path(paths))
}
pub fn save(paths: &AppPaths, config: &CoordinatorConfig) -> Result<(), ServiceError> {
write_state_record(&config_path(paths), config)
}
pub fn forget(paths: &AppPaths) -> Result<(), ServiceError> {
remove_state_record(&config_path(paths))?;
remove_state_record(&link_path(paths))
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct LinkRecord {
pub schema_version: u32,
pub written_at: DateTime<Utc>,
pub connected: bool,
pub since: Option<DateTime<Utc>>,
pub reason: Option<String>,
pub last_wake_at: Option<DateTime<Utc>>,
pub wakes_received: u64,
pub reconcile_interval_secs: Option<u32>,
pub covered: Vec<String>,
}
impl LinkRecord {
#[must_use]
pub fn new(written_at: DateTime<Utc>) -> Self {
Self {
schema_version: LINK_SCHEMA_VERSION,
written_at,
..Self::default()
}
}
}
pub fn record_link(paths: &AppPaths, link: &LinkRecord) -> Result<(), ServiceError> {
write_state_record(&link_path(paths), link)
}
pub fn last_link(paths: &AppPaths) -> Result<Option<LinkRecord>, ServiceError> {
read_state_record(&link_path(paths))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_setting_and_the_link_round_trip_and_leave_removes_both() {
let dir = tempfile::tempdir().expect("a temporary directory");
let paths = AppPaths::rooted_at(dir.path());
assert_eq!(configured(&paths).expect("absent"), None);
let config = CoordinatorConfig::new(
"https://runners.example.com".into(),
"h-1".into(),
"win-big".into(),
Utc::now(),
);
save(&paths, &config).expect("written");
assert_eq!(configured(&paths).expect("read"), Some(config));
let mut link = LinkRecord::new(Utc::now());
link.connected = true;
link.wakes_received = 3;
record_link(&paths, &link).expect("written");
assert_eq!(last_link(&paths).expect("read"), Some(link));
forget(&paths).expect("removed");
assert_eq!(configured(&paths).expect("absent"), None);
assert_eq!(last_link(&paths).expect("absent"), None);
forget(&paths).expect("removing nothing is fine");
}
}