use crate::application::error::{ApplicationError, ApplicationResult};
use crate::application::templates::bookmark_template::BookmarkTemplate;
use crate::domain::bookmark::Bookmark;
use crate::domain::error_context::ApplicationErrorContext;
use crate::domain::system_tag::SystemTag;
use std::fmt::Debug;
use std::fs::{self};
use std::io::Write;
use std::process::Command;
use tempfile::NamedTempFile;
use tracing::{debug, instrument};
pub trait TemplateService: Send + Sync + Debug {
fn edit_bookmark_with_template(
&self,
bookmark: Option<Bookmark>,
) -> ApplicationResult<(Bookmark, bool)>;
}
#[derive(Debug)]
pub struct TemplateServiceImpl {
editor: String,
}
impl Default for TemplateServiceImpl {
fn default() -> Self {
Self::new()
}
}
impl TemplateServiceImpl {
pub fn new() -> Self {
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
Self { editor }
}
pub fn with_editor(editor: String) -> Self {
Self { editor }
}
}
impl TemplateService for TemplateServiceImpl {
#[instrument(skip(self, bookmark), level = "debug")]
fn edit_bookmark_with_template(
&self,
bookmark: Option<Bookmark>,
) -> ApplicationResult<(Bookmark, bool)> {
let template = if let Some(ref bm) = bookmark {
BookmarkTemplate::from_bookmark(bm)
} else {
BookmarkTemplate::for_type(SystemTag::Uri)
};
let mut temp_file = NamedTempFile::new().app_context("Failed to create temporary file")?;
debug!("Temporary file for editing: {:?}", temp_file.path());
temp_file
.write_all(template.to_string().as_bytes())
.app_context("Failed to write to temporary file")?;
temp_file
.flush()
.app_context("Failed to flush temporary file")?;
let path = temp_file.path().to_path_buf();
let modified_before = fs::metadata(&path)?.modified()?;
let status = Command::new(&self.editor)
.arg(temp_file.path())
.status()
.app_context("Failed to open editor")?;
if !status.success() {
return Err(ApplicationError::Other(
"Editor exited with error".to_string(),
));
}
let modified_after = fs::metadata(&path)?.modified()?;
let was_modified = modified_after > modified_before;
let edited_content =
fs::read_to_string(temp_file.path()).app_context("Failed to read temporary file")?;
let edited_template = BookmarkTemplate::from_string(&edited_content)?;
let bookmark = edited_template.to_bookmark(bookmark.as_ref())?;
Ok((bookmark, was_modified))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::tag::Tag;
use crate::util::testing::init_test_env;
use std::collections::HashSet;
#[test]
#[ignore = "Manual test"]
fn given_bookmark_when_edit_with_template_then_returns_modified_bookmark() {
let _ = init_test_env();
let mut tags = HashSet::new();
tags.insert(Tag::new("test").unwrap());
let bookmark = Bookmark::new(
"https://example.com",
"Example Site",
"This is a description",
tags,
&crate::infrastructure::embeddings::DummyEmbedding,
)
.unwrap();
let service = TemplateServiceImpl::with_editor("vim".to_string());
let (_result, edited) = service.edit_bookmark_with_template(Some(bookmark)).unwrap();
assert!(edited, "Should detect file was modified");
}
}