use std::error::Error; use std::path::{Path, PathBuf}; use std::{fs, io}; use crate::domain; use crate::origin::Descr; use hex::ToHex; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] /// Values which the owner of a domain can configure when they install a domain. pub struct Config { pub origin_descr: Descr, } impl Config { pub fn hash(&self) -> Result> { let mut h = Sha256::new(); serde_json::to_writer(&mut h, self)?; Ok(h.finalize().encode_hex::()) } } #[derive(thiserror::Error, Debug)] pub enum GetError { #[error("not found")] NotFound, #[error(transparent)] Unexpected(Box), } #[derive(thiserror::Error, Debug)] pub enum SetError { #[error(transparent)] Unexpected(Box), } #[mockall::automock] pub trait Store: std::marker::Send + std::marker::Sync { fn get(&self, domain: &domain::Name) -> Result; fn set(&self, domain: &domain::Name, config: &Config) -> Result<(), SetError>; } struct FSStore { dir_path: PathBuf, } pub fn new(dir_path: &Path) -> io::Result { fs::create_dir_all(dir_path)?; Ok(FSStore { dir_path: dir_path.into(), }) } impl FSStore { fn config_dir_path(&self, domain: &domain::Name) -> PathBuf { self.dir_path.join(domain.as_str()) } fn config_file_path(&self, domain: &domain::Name) -> PathBuf { self.config_dir_path(domain).join("config.json") } } impl Store for FSStore { fn get(&self, domain: &domain::Name) -> Result { let config_file = fs::File::open(self.config_file_path(domain)).map_err(|e| match e.kind() { io::ErrorKind::NotFound => GetError::NotFound, _ => GetError::Unexpected(Box::from(e)), })?; serde_json::from_reader(config_file).map_err(|e| GetError::Unexpected(Box::from(e))) } fn set(&self, domain: &domain::Name, config: &Config) -> Result<(), SetError> { fs::create_dir_all(self.config_dir_path(domain)) .map_err(|e| SetError::Unexpected(Box::from(e)))?; let config_file = fs::File::create(self.config_file_path(domain)) .map_err(|e| SetError::Unexpected(Box::from(e)))?; serde_json::to_writer(config_file, config) .map_err(|e| SetError::Unexpected(Box::from(e)))?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::domain; use crate::origin::Descr; use std::str::FromStr; use tempdir::TempDir; #[test] fn basic() { let tmp_dir = TempDir::new("domain_config_store").unwrap(); let store = new(tmp_dir.path()).expect("store created"); let domain = domain::Name::from_str("foo.com").expect("domain parsed"); let config = Config { origin_descr: Descr::Git { url: "bar".to_string(), branch_name: "baz".to_string(), }, }; assert!(matches!( store.get(&domain), Err::(GetError::NotFound) )); store.set(&domain, &config).expect("config set"); assert_eq!(config, store.get(&domain).expect("config retrieved")); let new_config = Config { origin_descr: Descr::Git { url: "BAR".to_string(), branch_name: "BAZ".to_string(), }, }; store.set(&domain, &new_config).expect("config set"); assert_eq!(new_config, store.get(&domain).expect("config retrieved")); } }