use crate::paths::Paths;
use std::fs;
use std::io;
#[derive(Debug, Clone)]
pub enum Key {
Ask(String),
Answer(String),
Claim(String),
}
#[derive(Debug, Clone, Copy)]
pub enum Collection {
Asks,
Answers,
Claims,
}
pub trait StateStore {
fn read(&self, key: &Key) -> Option<String>;
fn exists(&self, key: &Key) -> bool;
fn write_atomic(&self, key: &Key, contents: &str) -> io::Result<()>;
fn create_exclusive(&self, key: &Key, contents: &str) -> io::Result<bool>;
fn remove(&self, key: &Key) -> io::Result<()>;
fn list(&self, collection: &Collection) -> Vec<String>;
}
pub struct FileStore<'a> {
paths: &'a Paths,
}
impl<'a> FileStore<'a> {
pub fn new(paths: &'a Paths) -> Self {
FileStore { paths }
}
fn path(&self, key: &Key) -> std::path::PathBuf {
match key {
Key::Ask(id) => self.paths.asks_dir().join(format!("{id}.json")),
Key::Answer(id) => self.paths.answers_dir().join(format!("{id}.json")),
Key::Claim(name) => self.paths.claims_dir().join(format!("{name}.json")),
}
}
fn dir(&self, c: &Collection) -> std::path::PathBuf {
match c {
Collection::Asks => self.paths.asks_dir(),
Collection::Answers => self.paths.answers_dir(),
Collection::Claims => self.paths.claims_dir(),
}
}
}
impl StateStore for FileStore<'_> {
fn read(&self, key: &Key) -> Option<String> {
fs::read_to_string(self.path(key)).ok()
}
fn exists(&self, key: &Key) -> bool {
self.path(key).is_file()
}
fn write_atomic(&self, key: &Key, contents: &str) -> io::Result<()> {
crate::util::write_atomic(&self.path(key), contents.as_bytes())
}
fn create_exclusive(&self, key: &Key, contents: &str) -> io::Result<bool> {
let path = self.path(key);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(mut f) => {
use io::Write;
f.write_all(contents.as_bytes())?;
Ok(true)
}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(false),
Err(e) => Err(e),
}
}
fn remove(&self, key: &Key) -> io::Result<()> {
match fs::remove_file(self.path(key)) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
fn list(&self, collection: &Collection) -> Vec<String> {
let mut names: Vec<String> = fs::read_dir(self.dir(collection))
.into_iter()
.flatten()
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().map(|x| x == "json").unwrap_or(false))
.filter_map(|p| p.file_stem().map(|s| s.to_string_lossy().to_string()))
.collect();
names.sort();
names
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_read_remove_round_trip() {
let p = Paths::temp();
let s = FileStore::new(&p);
let k = Key::Ask("w-1".into());
assert!(!s.exists(&k));
s.write_atomic(&k, "hello").unwrap();
assert!(s.exists(&k));
assert_eq!(s.read(&k).as_deref(), Some("hello"));
s.remove(&k).unwrap();
assert!(!s.exists(&k));
s.remove(&k).unwrap();
}
#[test]
fn create_exclusive_is_a_test_and_set() {
let p = Paths::temp();
let s = FileStore::new(&p);
let k = Key::Claim("repo".into());
assert!(s.create_exclusive(&k, "first").unwrap(), "first wins");
assert!(
!s.create_exclusive(&k, "second").unwrap(),
"second sees it already exists"
);
assert_eq!(s.read(&k).as_deref(), Some("first"), "loser never clobbers");
}
#[test]
fn list_returns_sorted_stems() {
let p = Paths::temp();
let s = FileStore::new(&p);
s.write_atomic(&Key::Claim("b".into()), "{}").unwrap();
s.write_atomic(&Key::Claim("a".into()), "{}").unwrap();
assert_eq!(s.list(&Collection::Claims), vec!["a", "b"]);
}
}