//! A wrapper around an object that ties it to a physical file extern crate serde; use anyhow::Context; use serde::de::DeserializeOwned; use serde::Serialize; use std::fs::File; use std::path::{Path, PathBuf}; use thiserror::Error; #[derive(Error, Debug)] pub enum Error { #[error(transparent)] Serialization(bincode::Error), #[error(transparent)] IO(std::io::Error), #[error(transparent)] Other(#[from] anyhow::Error), } /// A wrapper around an object `T` that ties the object to a physical file #[derive(Debug)] pub struct FileLinked where T: Serialize, { val: T, path: PathBuf, } impl FileLinked where T: Serialize, { /// Returns a readonly reference of `T` /// /// # Examples /// ``` /// # use file_linked::*; /// # use serde::{Deserialize, Serialize}; /// # use std::fmt; /// # use std::string::ToString; /// # use std::path::PathBuf; /// # /// # #[derive(Deserialize, Serialize)] /// # struct Test { /// # pub a: u32, /// # pub b: String, /// # pub c: f64 /// # } /// # /// # fn main() { /// let test = Test { /// a: 1, /// b: String::from("two"), /// c: 3.0 /// }; /// /// let linked_test = FileLinked::new(test, &PathBuf::from("./temp")) /// .expect("Unable to create file linked object"); /// /// assert_eq!(linked_test.readonly().a, 1); /// assert_eq!(linked_test.readonly().b, String::from("two")); /// assert_eq!(linked_test.readonly().c, 3.0); /// # /// # std::fs::remove_file("./temp").expect("Unable to remove file"); /// # } /// ``` pub fn readonly(&self) -> &T { &self.val } /// Creates a new [`FileLinked`] object of type `T` stored to the file given by `path`. /// /// # Examples /// ``` /// # use file_linked::*; /// # use serde::{Deserialize, Serialize}; /// # use std::fmt; /// # use std::string::ToString; /// # use std::path::PathBuf; /// # /// #[derive(Deserialize, Serialize)] /// struct Test { /// pub a: u32, /// pub b: String, /// pub c: f64 /// } /// /// # fn main() { /// let test = Test { /// a: 1, /// b: String::from("two"), /// c: 3.0 /// }; /// /// let linked_test = FileLinked::new(test, &PathBuf::from("./temp")) /// .expect("Unable to create file linked object"); /// /// assert_eq!(linked_test.readonly().a, 1); /// assert_eq!(linked_test.readonly().b, String::from("two")); /// assert_eq!(linked_test.readonly().c, 3.0); /// # /// # std::fs::remove_file("./temp").expect("Unable to remove file"); /// # } /// ``` pub fn new(val: T, path: &Path) -> Result, Error> { let result = FileLinked { val, path: path.to_path_buf(), }; result.write_data()?; Ok(result) } fn write_data(&self) -> Result<(), Error> { let file = File::create(&self.path) .with_context(|| format!("Unable to open path {}", self.path.display()))?; bincode::serialize_into(file, &self.val) .with_context(|| format!("Unable to write to file {}", self.path.display()))?; Ok(()) } /// Modifies the data contained in a `FileLinked` object using a callback `op` that has a mutable reference to the /// underlying data. After the mutable operation is performed the data is written to a file to synchronize the state. /// /// # Examples /// ``` /// # use file_linked::*; /// # use serde::{Deserialize, Serialize}; /// # use std::fmt; /// # use std::string::ToString; /// # use std::path::PathBuf; /// # /// # #[derive(Deserialize, Serialize)] /// # struct Test { /// # pub a: u32, /// # pub b: String, /// # pub c: f64 /// # } /// # /// # fn main() -> Result<(), Error> { /// let test = Test { /// a: 1, /// b: String::from(""), /// c: 0.0 /// }; /// /// let mut linked_test = FileLinked::new(test, &PathBuf::from("./temp")) /// .expect("Unable to create file linked object"); /// /// assert_eq!(linked_test.readonly().a, 1); /// /// linked_test.mutate(|t| t.a = 2)?; /// /// assert_eq!(linked_test.readonly().a, 2); /// # /// # std::fs::remove_file("./temp").expect("Unable to remove file"); /// # /// # Ok(()) /// # } /// ``` pub fn mutate U>(&mut self, op: F) -> Result { let result = op(&mut self.val); self.write_data()?; Ok(result) } /// Replaces the value held by the `FileLinked` object with `val`. After replacing the object will be written to a file. /// /// # Examples /// ``` /// # use file_linked::*; /// # use serde::{Deserialize, Serialize}; /// # use std::fmt; /// # use std::string::ToString; /// # use std::path::PathBuf; /// # /// # #[derive(Deserialize, Serialize)] /// # struct Test { /// # pub a: u32, /// # pub b: String, /// # pub c: f64 /// # } /// # /// # fn main() -> Result<(), Error> { /// let test = Test { /// a: 1, /// b: String::from(""), /// c: 0.0 /// }; /// /// let mut linked_test = FileLinked::new(test, &PathBuf::from("./temp")) /// .expect("Unable to create file linked object"); /// /// assert_eq!(linked_test.readonly().a, 1); /// /// linked_test.replace(Test { /// a: 2, /// b: String::from(""), /// c: 0.0 /// })?; /// /// assert_eq!(linked_test.readonly().a, 2); /// # /// # std::fs::remove_file("./temp").expect("Unable to remove file"); /// # /// # Ok(()) /// # } /// ``` pub fn replace(&mut self, val: T) -> Result<(), Error> { self.val = val; self.write_data() } } impl FileLinked where T: Serialize + DeserializeOwned, { /// Deserializes an object `T` from the file given by `path` /// /// # Examples /// ``` /// # use file_linked::*; /// # use serde::{Deserialize, Serialize}; /// # use std::fmt; /// # use std::string::ToString; /// # use std::fs; /// # use std::fs::OpenOptions; /// # use std::io::Write; /// # use std::path::PathBuf; /// # /// # #[derive(Deserialize, Serialize)] /// # struct Test { /// # pub a: u32, /// # pub b: String, /// # pub c: f64 /// # } /// # /// # fn main() -> Result<(), Error> { /// let test = Test { /// a: 1, /// b: String::from("2"), /// c: 3.0 /// }; /// /// let path = PathBuf::from("./temp"); /// /// let mut file = OpenOptions::new() /// .write(true) /// .create(true) /// .open(&path) /// .expect("Unable to create file"); /// /// bincode::serialize_into(file, &test).expect("Unable to serialize object"); /// /// let mut linked_test = FileLinked::::from_file(&path) /// .expect("Unable to create file linked object"); /// /// assert_eq!(linked_test.readonly().a, test.a); /// assert_eq!(linked_test.readonly().b, test.b); /// assert_eq!(linked_test.readonly().c, test.c); /// # /// # std::fs::remove_file("./temp").expect("Unable to remove file"); /// # /// # Ok(()) /// # } /// ``` pub fn from_file(path: &Path) -> Result, Error> { let file = File::open(path).with_context(|| format!("Unable to open file {}", path.display()))?; let val = bincode::deserialize_from(file) .with_context(|| String::from("Unable to parse value from file."))?; Ok(FileLinked { val, path: path.to_path_buf(), }) } } #[cfg(test)] mod tests { use super::*; use std::fs; #[test] fn test_mutate() -> Result<(), Error> { let list = vec![1, 2, 3, 4]; let mut file_linked_list = FileLinked::new(list, &PathBuf::from("test.txt"))?; assert_eq!(format!("{:?}", file_linked_list.readonly()), "[1, 2, 3, 4]"); file_linked_list.mutate(|v1| v1.push(5))?; assert_eq!( format!("{:?}", file_linked_list.readonly()), "[1, 2, 3, 4, 5]" ); file_linked_list.mutate(|v1| v1[1] = 1)?; assert_eq!( format!("{:?}", file_linked_list.readonly()), "[1, 1, 3, 4, 5]" ); fs::remove_file("test.txt").expect("Unable to remove file"); Ok(()) } }