domani/src/domain/config.rs

168 lines
5.0 KiB
Rust
Raw Normal View History

2023-05-07 16:07:31 +00:00
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::{fs, io, sync};
2023-05-07 16:07:31 +00:00
use crate::error::unexpected::{self, Intoable, Mappable};
use crate::{domain, origin};
2023-05-07 16:07:31 +00:00
2023-05-09 14:40:40 +00:00
use hex::ToHex;
2023-05-07 16:07:31 +00:00
use serde::{Deserialize, Serialize};
2023-05-09 14:40:40 +00:00
use sha2::{Digest, Sha256};
2023-05-07 16:07:31 +00:00
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
2023-05-07 16:07:31 +00:00
/// Values which the owner of a domain can configure when they install a domain.
pub struct Config {
pub origin_descr: origin::Descr,
2023-05-07 16:07:31 +00:00
}
2023-05-09 14:40:40 +00:00
impl Config {
pub fn hash(&self) -> Result<String, unexpected::Error> {
2023-05-09 14:40:40 +00:00
let mut h = Sha256::new();
serde_json::to_writer(&mut h, self).or_unexpected()?;
2023-05-09 14:40:40 +00:00
Ok(h.finalize().encode_hex::<String>())
}
}
#[derive(thiserror::Error, Debug)]
2023-05-07 16:07:31 +00:00
pub enum GetError {
#[error("not found")]
2023-05-07 16:07:31 +00:00
NotFound,
#[error(transparent)]
Unexpected(#[from] unexpected::Error),
2023-05-07 16:07:31 +00:00
}
#[derive(thiserror::Error, Debug)]
2023-05-07 16:07:31 +00:00
pub enum SetError {
#[error(transparent)]
Unexpected(#[from] unexpected::Error),
2023-05-07 16:07:31 +00:00
}
/// Used in the return from all_domains from Store.
pub type AllDomainsResult<T> = Result<T, unexpected::Error>;
2023-05-09 14:37:06 +00:00
#[mockall::automock]
pub trait Store {
fn get(&self, domain: &domain::Name) -> Result<Config, GetError>;
fn set(&self, domain: &domain::Name, config: &Config) -> Result<(), SetError>;
fn all_domains(&self) -> AllDomainsResult<Vec<AllDomainsResult<domain::Name>>>;
2023-05-07 16:07:31 +00:00
}
pub trait BoxedStore: Store + Send + Sync + Clone {}
2023-05-09 14:37:06 +00:00
struct FSStore {
2023-05-07 16:07:31 +00:00
dir_path: PathBuf,
}
pub fn new(dir_path: &Path) -> io::Result<impl BoxedStore> {
2023-05-09 14:37:06 +00:00
fs::create_dir_all(dir_path)?;
Ok(sync::Arc::new(FSStore {
2023-05-09 14:37:06 +00:00
dir_path: dir_path.into(),
}))
2023-05-09 14:37:06 +00:00
}
2023-05-07 16:07:31 +00:00
2023-05-09 14:37:06 +00:00
impl FSStore {
fn config_dir_path(&self, domain: &domain::Name) -> PathBuf {
self.dir_path.join(domain.as_str())
2023-05-07 16:07:31 +00:00
}
fn config_file_path(&self, domain: &domain::Name) -> PathBuf {
2023-05-07 16:07:31 +00:00
self.config_dir_path(domain).join("config.json")
}
}
impl BoxedStore for sync::Arc<FSStore> {}
impl Store for sync::Arc<FSStore> {
fn get(&self, domain: &domain::Name) -> Result<Config, GetError> {
let path = self.config_file_path(domain);
let config_file = fs::File::open(path.as_path()).map_err(|e| match e.kind() {
io::ErrorKind::NotFound => GetError::NotFound,
_ => e
.into_unexpected_while(format!("opening {}", path.display()))
.into(),
})?;
let config = serde_json::from_reader(config_file)
.map_unexpected_while(|| format!("json parsing {}", path.display()))?;
Ok(config)
2023-05-07 16:07:31 +00:00
}
fn set(&self, domain: &domain::Name, config: &Config) -> Result<(), SetError> {
let dir_path = self.config_dir_path(domain);
fs::create_dir_all(dir_path.as_path())
.map_unexpected_while(|| format!("creating dir {}", dir_path.display()))?;
let file_path = self.config_file_path(domain);
let config_file = fs::File::create(file_path.as_path())
.map_unexpected_while(|| format!("creating file {}", file_path.display()))?;
2023-05-07 16:07:31 +00:00
serde_json::to_writer(config_file, config)
.map_unexpected_while(|| format!("writing config to {}", file_path.display()))?;
2023-05-07 16:07:31 +00:00
Ok(())
}
fn all_domains(&self) -> AllDomainsResult<Vec<AllDomainsResult<domain::Name>>> {
Ok(fs::read_dir(&self.dir_path)
.or_unexpected()?
.map(
|dir_entry_res: io::Result<fs::DirEntry>| -> AllDomainsResult<domain::Name> {
let domain = dir_entry_res.or_unexpected()?.file_name();
let domain = domain.to_str().ok_or(unexpected::Error::from(
"couldn't convert os string to &str",
))?;
domain::Name::from_str(domain)
.map_unexpected_while(|| format!("parsing {domain} as domain name"))
},
)
.collect())
}
2023-05-07 16:07:31 +00:00
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain;
2023-05-07 16:07:31 +00:00
use crate::origin::Descr;
use std::str::FromStr;
2023-05-07 16:07:31 +00:00
use tempdir::TempDir;
#[test]
fn basic() {
let tmp_dir = TempDir::new("domain_config_store").unwrap();
2023-05-11 12:31:48 +00:00
let store = new(tmp_dir.path()).expect("store created");
2023-05-07 16:07:31 +00:00
let domain = domain::Name::from_str("foo.com").expect("domain parsed");
2023-05-07 16:07:31 +00:00
let config = Config {
origin_descr: Descr::Git {
url: "bar".to_string(),
branch_name: "baz".to_string(),
},
};
assert!(matches!(
store.get(&domain),
2023-05-07 16:07:31 +00:00
Err::<Config, GetError>(GetError::NotFound)
));
store.set(&domain, &config).expect("config set");
assert_eq!(config, store.get(&domain).expect("config retrieved"));
2023-05-07 16:07:31 +00:00
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"));
2023-05-07 16:07:31 +00:00
}
}