use std::fs::{self, File};
use std::io;
use std::io::prelude::*;
use std::num::NonZeroU32;
use std::path::Path;
use std::result;
#[doc(inline)]
use crate::error::{Error, PathError};
#[doc(inline)]
use crate::file;
#[derive(Debug)]
pub struct Stack(String);
impl Stack {
pub fn new(file: &str) -> result::Result<Self, PathError> {
file::canonical_filename(file, file::FileKind::StackFile).map(Self)
}
pub fn open(&self) -> result::Result<File, PathError> {
File::open(&self.0).map_err(|e| PathError::FileAccess(self.clone_file(), e.to_string()))
}
fn clone_file(&self) -> String { self.0.clone() }
pub fn exists(&self) -> bool { Path::new(&self.0).exists() }
pub fn clear(&self) -> result::Result<(), PathError> {
fs::remove_file(&self.0).map_err(|e| PathError::FileAccess(self.clone_file(), e.to_string()))
}
pub fn push(&self, task: &str) -> result::Result<(), PathError> {
let file = file::append_open(&self.0)?;
let mut stream = io::BufWriter::new(file);
writeln!(&mut stream, "{task}")
.map_err(|e| PathError::FileWrite(self.clone_file(), e.to_string()))?;
stream
.flush()
.map_err(|e| PathError::FileWrite(self.clone_file(), e.to_string()))?;
Ok(())
}
pub fn pop(&self) -> Option<String> {
let mut file = file::rw_open(&self.0).ok()?;
file::pop_last_line(&mut file)
}
pub fn drop(&self, num: NonZeroU32) -> crate::Result<()> {
(0..num.get())
.try_for_each(|_| self.pop().map(|_| ()))
.ok_or(Error::StackPop)
}
pub fn keep(&self, num: NonZeroU32) -> crate::Result<()> {
let file = self.open()?;
let unum = num.get() as usize;
let len = io::BufReader::new(file).lines().count();
if len > unum {
let backfile = format!("{}-bak", self.0);
let outfile = file::append_open(&backfile)?;
let mut stream = io::BufWriter::new(outfile);
let reader = io::BufReader::new(self.open()?);
for line in reader.lines().skip(len - unum).flatten() {
writeln!(&mut stream, "{line}")
.map_err(|e| PathError::FileWrite(self.clone_file(), e.to_string()))?;
}
stream
.flush()
.map_err(|e| PathError::FileWrite(self.clone_file(), e.to_string()))?;
fs::rename(&backfile, self.clone_file())
.map_err(|e| PathError::RenameFailure(self.clone_file(), e.to_string()))?;
}
Ok(())
}
pub fn process_down_stack<F>(&self, mut func: F) -> crate::Result<()>
where
F: FnMut(usize, &str)
{
let file = self.open()?;
let lines: Vec<String> = io::BufReader::new(file).lines().map_while(Result::ok).collect();
for (i, ln) in lines.iter().rev().enumerate() {
func(i, ln);
}
Ok(())
}
pub fn list(&self) -> String {
let mut output = String::new();
let Ok(_) = self.process_down_stack(|_, l| {
output.push_str(l);
output.push('\n');
})
else {
return String::new();
};
output
}
pub fn top(&self) -> crate::Result<String> {
let file = self.open()?;
let reader = io::BufReader::new(file).lines().map_while(Result::ok);
Ok(reader.last().unwrap_or_default())
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use assert2::{assert, let_assert};
use nzliteral::nzliteral;
use tempfile::TempDir;
use super::*;
#[test]
fn test_new_missing_file() {
let_assert!(Err(err) = Stack::new(""));
assert!(err == PathError::FilenameMissing);
}
#[test]
fn test_new_bad_path() {
let mut stackdir = TempDir::new()
.expect("Cannot make tempdir")
.path()
.to_path_buf();
stackdir.push("foo");
stackdir.push("stack.txt");
let_assert!(Some(file) = stackdir.as_path().to_str());
let_assert!(Err(e) = Stack::new(file));
assert!(e == PathError::InvalidPath(
file.to_string(),
"No such file or directory (os error 2)".to_string()
));
}
#[test]
fn test_new() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
assert!(Stack::new(filename).is_ok());
}
#[test]
fn test_push() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
let_assert!(Ok(stack) = Stack::new(filename));
assert!(!Path::new(filename).exists());
let task = "+house @todo change filters";
assert!(stack.push(task).is_ok());
let path = Path::new(filename);
assert!(path.is_file());
let_assert!(Ok(filelen) = path.metadata().map(|m| m.len() as usize));
assert!(filelen == task.len() + 1);
assert!(stack.push(task).is_ok());
let_assert!(Ok(filelen) = path.metadata().map(|m| m.len() as usize));
assert!(filelen == 2 * task.len() + 2);
}
#[test]
fn test_pop() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
let_assert!(Ok(mut file) = File::create(filename));
let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n"));
let_assert!(Ok(stack) = Stack::new(filename));
let_assert!(Some(line) = stack.pop());
assert!(line == String::from("+home @todo second"));
let_assert!(Some(line) = stack.pop());
assert!(line == String::from("+home @todo first"));
let path = Path::new(filename);
assert!(path.is_file());
let_assert!(Ok(filelen) = path.metadata().map(|m| m.len() as usize));
assert!(filelen == 0);
}
#[test]
fn test_clear() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
let_assert!(Ok(mut file) = File::create(filename));
let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n"));
let_assert!(Ok(stack) = Stack::new(filename));
assert!(stack.clear().is_ok());
assert!(!Path::new(filename).exists());
}
#[test]
fn test_drop_1() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
let_assert!(Ok(mut file) = File::create(filename));
let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n"), "Cannot fill file");
let_assert!(Ok(stack) = Stack::new(filename));
assert!(stack.drop(nzliteral!(1u32)).is_ok());
let_assert!(Some(line) = stack.pop());
assert!(line == String::from("+home @todo first"));
}
#[test]
fn test_drop_2() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
let_assert!(Ok(mut file) = File::create(filename));
let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n"));
let_assert!(Ok(stack) = Stack::new(filename));
assert!(stack.drop(nzliteral!(2u32)).is_ok());
let_assert!(Some(line) = stack.pop());
assert!(line == String::from("+home @todo first"));
}
#[test]
fn test_keep_1() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
let_assert!(Ok(mut file) = File::create(filename));
let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n"));
let_assert!(Ok(stack) = Stack::new(filename));
assert!(stack.keep(nzliteral!(1u32)).is_ok());
let_assert!(Some(line) = stack.pop());
assert!(line == String::from("+home @todo second"));
assert!(stack.pop().is_none());
}
#[test]
fn test_keep_2() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
let_assert!(Ok(mut file) = File::create(filename));
let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n"));
let_assert!(Ok(stack) = Stack::new(filename));
assert!(stack.keep(nzliteral!(2u32)).is_ok());
let_assert!(Some(line) = stack.pop());
assert!(line == String::from("+home @todo third"));
let_assert!(Some(line) = stack.pop());
assert!(line == String::from("+home @todo second"));
assert!(stack.pop().is_none());
}
#[test]
fn test_list() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
let_assert!(Ok(mut file) = File::create(filename));
let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n"));
let_assert!(Ok(stack) = Stack::new(filename));
assert!(stack.list() == String::from(
"+home @todo third\n+home @todo second\n+home @todo first\n"
));
}
#[test]
fn test_top() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
let_assert!(Ok(mut file) = File::create(filename));
let_assert!(Ok(_) = file.write_all(b"+home @todo first\n+home @todo second\n+home @todo third\n"));
let_assert!(Ok(stack) = Stack::new(filename));
let_assert!(Ok(line) = stack.top());
assert!(line == String::from("+home @todo third"));
}
#[test]
fn test_top_empty() {
let_assert!(Ok(tmpdir) = TempDir::new());
let mut path = tmpdir.path().to_path_buf();
path.push("stack.txt");
let_assert!(Some(filename) = path.to_str());
let_assert!(Ok(_) = File::create(filename));
let_assert!(Ok(stack) = Stack::new(filename));
let_assert!(Ok(line) = stack.top());
assert!(line == String::new());
}
}