tui-journal 0.17.0

Tui app allows writing and managing journals/notes from within the terminal With different local back-ends
Documentation
use std::path::PathBuf;

use anyhow::Context;

use super::*;

pub struct JsonDataProvide {
    file_path: PathBuf,
}

impl JsonDataProvide {
    pub fn new(file_path: PathBuf) -> Self {
        Self { file_path }
    }
}

impl DataProvider for JsonDataProvide {
    async fn load_all_entries(&self) -> anyhow::Result<Vec<Entry>> {
        if !self.file_path.exists() {
            return Ok(Vec::new());
        }

        let json_content = tokio::fs::read_to_string(&self.file_path)
            .await
            .with_context(|| {
                format!("Failed to read entries file: {}", self.file_path.display())
            })?;

        if json_content.is_empty() {
            return Ok(Vec::new());
        }

        let entries = serde_json::from_str(&json_content).with_context(|| {
            format!("Failed to parse entries file: {}", self.file_path.display())
        })?;

        Ok(entries)
    }

    async fn add_entry(&self, entry: EntryDraft) -> Result<Entry, ModifyEntryError> {
        if entry.title.is_empty() {
            return Err(ModifyEntryError::ValidationError(
                "Entry title can't be empty".into(),
            ));
        }

        let mut entries = self.load_all_entries().await?;

        let id: u32 = entries.iter().map(|e| e.id + 1).max().unwrap_or(0);

        let new_entry = Entry::from_draft(id, entry);

        entries.push(new_entry);

        self.write_entries_to_file(&entries).await?;

        Ok(entries.into_iter().next_back().unwrap())
    }

    async fn restore_entry(&self, entry: Entry) -> Result<Entry, ModifyEntryError> {
        if entry.title.is_empty() {
            return Err(ModifyEntryError::ValidationError(
                "Entry title can't be empty".into(),
            ));
        }

        let mut entries = self.load_all_entries().await?;
        if entries.iter().any(|existing| existing.id == entry.id) {
            return Err(ModifyEntryError::ValidationError(format!(
                "Entry id {} already exists",
                entry.id
            )));
        }

        entries.push(entry.clone());
        self.write_entries_to_file(&entries).await?;

        Ok(entry)
    }

    async fn remove_entry(&self, entry_id: u32) -> anyhow::Result<()> {
        let mut entries = self.load_all_entries().await?;

        if let Some(pos) = entries.iter().position(|e| e.id == entry_id) {
            entries.remove(pos);

            self.write_entries_to_file(&entries).await?;
        }

        Ok(())
    }

    async fn update_entry(&self, entry: Entry) -> Result<Entry, ModifyEntryError> {
        if entry.title.is_empty() {
            return Err(ModifyEntryError::ValidationError(
                "Entry title can't be empty".into(),
            ));
        }

        let mut entries = self.load_all_entries().await?;

        if let Some(entry_to_modify) = entries.iter_mut().find(|e| e.id == entry.id) {
            *entry_to_modify = entry.clone();

            self.write_entries_to_file(&entries).await?;

            Ok(entry)
        } else {
            Err(ModifyEntryError::ValidationError(
                "Entry title can't be empty".into(),
            ))
        }
    }

    async fn get_export_object(&self, entries_ids: &[u32]) -> anyhow::Result<EntriesDTO> {
        let entries: Vec<EntryDraft> = self
            .load_all_entries()
            .await?
            .into_iter()
            .filter(|entry| entries_ids.contains(&entry.id))
            .map(EntryDraft::from_entry)
            .collect();

        Ok(EntriesDTO::new(entries))
    }

    async fn assign_priority_to_entries(&self, priority: u32) -> anyhow::Result<()> {
        let mut entries = self.load_all_entries().await?;

        let mut modified = false;

        entries
            .iter_mut()
            .filter(|entry| entry.priority.is_none())
            .for_each(|entry| {
                entry.priority = Some(priority);
                modified = true;
            });

        if modified {
            self.write_entries_to_file(&entries).await?;
        }

        Ok(())
    }
}

impl JsonDataProvide {
    async fn write_entries_to_file(&self, entries: &Vec<Entry>) -> anyhow::Result<()> {
        let entries_text = serde_json::to_vec(&entries).with_context(|| {
            format!(
                "Failed to serialize entries for {}",
                self.file_path.display()
            )
        })?;
        if !self.file_path.exists()
            && let Some(parent) = self.file_path.parent()
        {
            tokio::fs::create_dir_all(parent).await.with_context(|| {
                format!("Failed to create entries directory: {}", parent.display())
            })?;
        }
        tokio::fs::write(&self.file_path, entries_text)
            .await
            .with_context(|| {
                format!("Failed to write entries file: {}", self.file_path.display())
            })?;

        Ok(())
    }
}