use super::model::InfraState;
use super::txn::FileTree;
use anyhow::Result;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock};
pub struct Repo {
tree: FileTree,
changes: tokio::sync::watch::Sender<u64>,
state: RwLock<Arc<InfraState>>,
gate: Mutex<()>,
#[cfg(test)]
_tmp: Option<tempfile::TempDir>,
}
impl Repo {
pub fn open(dir: impl Into<PathBuf>) -> Result<Self> {
let tree = FileTree::open(dir)?;
let state = InfraState::read(&tree)?;
Ok(Self {
tree,
changes: tokio::sync::watch::channel(0).0,
state: RwLock::new(Arc::new(state.unwrap_or_default())),
gate: Mutex::new(()),
#[cfg(test)]
_tmp: None,
})
}
#[cfg(test)]
pub fn temp() -> Self {
let tmp = tempfile::tempdir().unwrap();
let mut r = Self::open(tmp.path().join("infra")).unwrap();
r._tmp = Some(tmp);
r
}
pub fn tree(&self) -> &FileTree {
&self.tree
}
pub fn subscribe(&self) -> tokio::sync::watch::Receiver<u64> {
self.changes.subscribe()
}
pub fn exclusive<R>(&self, f: impl FnOnce(&std::path::Path, &InfraState) -> R) -> R {
let _g = self.gate.lock().unwrap();
let state = self.state.read().unwrap().clone();
f(self.tree.root(), &state)
}
pub fn is_initialized(&self) -> Result<bool> {
Ok(self.tree.read("MANIFEST.json")?.is_some())
}
pub fn snapshot(&self) -> Arc<InfraState> {
self.state.read().unwrap().clone()
}
pub fn reload(&self) -> Result<()> {
let _g = self.gate.lock().unwrap();
let st = InfraState::read(&self.tree)?.unwrap_or_default();
*self.state.write().unwrap() = Arc::new(st);
self.changes.send_modify(|v| *v += 1);
Ok(())
}
pub fn update<R>(&self, f: impl FnOnce(&mut InfraState) -> Result<R>) -> Result<R> {
let _g = self.gate.lock().unwrap();
let mut next = (**self.state.read().unwrap()).clone();
let out = f(&mut next)?;
next.normalize();
if next == **self.state.read().unwrap() && self.is_initialized()? {
return Ok(out);
}
next.write(&self.tree)?;
*self.state.write().unwrap() = Arc::new(next);
self.changes.send_modify(|v| *v += 1);
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::infra::model::SettingsInfra;
#[test]
fn update_grava_e_sobrevive_a_reabrir() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("infra");
{
let r = Repo::open(&dir).unwrap();
assert!(!r.is_initialized().unwrap());
r.update(|s| {
s.settings = SettingsInfra {
acme_email: Some("a@b.c".into()),
..Default::default()
};
Ok(())
})
.unwrap();
assert_eq!(r.snapshot().settings.acme_email.as_deref(), Some("a@b.c"));
}
let r = Repo::open(&dir).unwrap();
assert_eq!(r.snapshot().settings.acme_email.as_deref(), Some("a@b.c"));
}
#[test]
fn erro_na_mudanca_ou_na_validacao_nao_deixa_rastro() {
let r = Repo::temp();
r.update(|s| {
s.settings.acme_email = Some("antes".into());
Ok(())
})
.unwrap();
let falha: Result<()> = r.update(|s| {
s.settings.acme_email = Some("depois".into());
anyhow::bail!("não")
});
assert!(falha.is_err());
let e: Result<()> = r.update(|s| {
for id in ["a", "b"] {
s.projects.push(crate::store::infra::model::ProjectInfra {
id: id.into(),
name: "igual".into(),
description: None,
env_vars: vec![],
env_comments: vec![],
network_name: None,
created_at: chrono::Utc::now(),
services: vec![],
jobs: vec![],
secrets: vec![],
managed_databases: vec![],
shared_access: vec![],
});
}
Ok(())
});
assert!(e.is_err());
assert_eq!(r.snapshot().settings.acme_email.as_deref(), Some("antes"));
assert!(r.snapshot().projects.is_empty());
let relido = InfraState::read(r.tree()).unwrap().unwrap();
assert!(relido.projects.is_empty());
}
#[tokio::test]
async fn escrita_efetiva_avisa_quem_assina_e_escrita_igual_nao() {
let r = Repo::temp();
let mut rx = r.subscribe();
r.update(|s| {
s.settings.acme_email = Some("a@b.c".into());
Ok(())
})
.unwrap();
assert!(rx.has_changed().unwrap());
rx.mark_unchanged();
r.update(|s| {
s.settings.acme_email = Some("a@b.c".into());
Ok(())
})
.unwrap();
assert!(!rx.has_changed().unwrap());
}
#[test]
fn exclusive_ve_a_raiz_e_o_estado_e_segura_os_escritores() {
let r = Repo::temp();
r.update(|s| {
s.settings.acme_email = Some("x".into());
Ok(())
})
.unwrap();
let (existe, email) = r.exclusive(|root, st| {
(
root.join("MANIFEST.json").exists(),
st.settings.acme_email.clone(),
)
});
assert!(existe);
assert_eq!(email.as_deref(), Some("x"));
}
}