lib-contra 5.0.2

The function implementation of contra
Documentation
//! Allow saving and loading to/from disk
use std::{
    fs::File,
    io::{self, BufReader, Cursor, Read, Write},
    path::Path,
    str::from_utf8,
};

use crate::{
    deserialize::{
        json::{FromJson, JsonDeserializer},
        Deserialize,
    },
    error::{AnyError, IoResult},
    serialize::{
        json::{IntoJson, JsonSerializer, PrettyJsonFormatter},
        Serialize,
    },
};

/// Allow saving and loading to/from disk
///
/// Automatically implemented for types that implement both [Serialize] and [Deserialize]
pub trait Persistent: Serialize + Deserialize {
    fn save(&self, path: &str) -> Result<(), AnyError>;
    fn load(path: &str) -> Result<Self, AnyError>;
}

fn serialize_with_default<S: Serialize>(value: &S) -> Result<Vec<u8>, AnyError> {
    let mut buffer = Vec::with_capacity(128);
    let mut ser = DefaultSerializer::new(PrettyJsonFormatter::new("\t".to_string()), &mut buffer);
    value.serialize(&mut ser, &crate::position::Position::Closing)?;
    Ok(buffer)
}

fn deserialize_with_default<D: Deserialize>(value: &[u8]) -> Result<D, AnyError> {
    let cursor = Cursor::new(value);
    let mut des = DefaultDeserializer::new(cursor);
    D::deserialize(&mut des)
}

fn serialize_factory<S: Serialize>(value: &S, path: &Path) -> Result<Vec<u8>, AnyError> {
    if let Some(ending) = path.extension() {
        if ending == "json" {
            return IntoJson::to_json(value).map(|json| json.into_bytes());
        }
    }
    serialize_with_default(value)
}

fn deserializer_factory<D: Deserialize>(value: &[u8], path: &Path) -> Result<D, AnyError> {
    if let Some(ending) = path.extension() {
        if ending == "json" {
            return FromJson::from_json(
                &from_utf8(value).expect("failed to convert content to utf8"),
            );
        }
    }
    deserialize_with_default(value)
}

type DefaultSerializer<'w> = JsonSerializer<'w, Vec<u8>, PrettyJsonFormatter>;
type DefaultDeserializer<'w> = JsonDeserializer<Cursor<&'w [u8]>>;

impl<T: Sized + Serialize + Deserialize> Persistent for T {
    fn save(&self, path: &str) -> Result<(), AnyError> {
        let path = Path::new(path);
        let buffer = serialize_factory(self, path)?;
        write_bytes_file(buffer.as_slice(), path).map_err(|e| e.into())
    }

    fn load(path: &str) -> Result<Self, AnyError> {
        let path = Path::new(path);
        let content = read_bytes_file(path)?;
        deserializer_factory(content.as_slice(), path)
    }
}

fn write_bytes_file(bytes: &[u8], path: &Path) -> IoResult {
    let mut f = File::create(path)?;
    f.write_all(bytes)?;
    Ok(())
}

fn read_bytes_file(path: &Path) -> Result<Vec<u8>, io::Error> {
    let file = File::open(path)?;
    let mut reader = BufReader::new(file);
    let mut buffer = Vec::new();

    reader.read_to_end(&mut buffer)?;

    Ok(buffer)
}

#[cfg(test)]
mod test {
    use std::{
        fs::{self},
        path::Path,
    };

    use super::Persistent;

    struct FileLifetime {
        pub(crate) path: String,
    }

    impl Drop for FileLifetime {
        fn drop(&mut self) {
            let path = Path::new(&self.path);
            if path.exists() {
                if !path.is_dir() {
                    fs::remove_file(&path)
                        .expect(format!("failed to delete file: {}", self.path).as_str());
                } else {
                    fs::remove_dir_all(&path)
                        .expect(format!("failed to delete directory: {}", self.path).as_str());
                }
            }
        }
    }

    #[test]
    fn save_and_then_load_works() {
        let file_lifetime = FileLifetime {
            path: "save_i32.json".to_string(),
        };
        let data = 32i32;

        let saved = data.save(&file_lifetime.path);

        assert!(saved.is_ok());
        assert!(Path::new(&file_lifetime.path).exists());

        let loaded = i32::load(&file_lifetime.path);

        assert!(loaded.is_ok());
        assert_eq!(data, loaded.unwrap());
    }
}