use crate::domain::error::{DomainError, DomainResult, RepositoryError};
use crate::domain::repositories::import_repository::{BookmarkImportData, ImportRepository};
use crate::domain::tag::Tag;
use crossterm::style::Stylize;
use serde::Deserialize;
use std::collections::HashSet;
use std::fs::File;
use std::io::{BufReader, Read};
use tracing::{debug, warn};
#[derive(Deserialize)]
struct JsonBookmark {
url: String,
title: String,
description: String,
tags: Vec<String>,
}
#[derive(Debug)]
pub struct JsonImportRepository;
impl Default for JsonImportRepository {
fn default() -> Self {
Self::new()
}
}
impl JsonImportRepository {
pub fn new() -> Self {
Self
}
}
impl ImportRepository for JsonImportRepository {
fn import_json_bookmarks(&self, path: &str) -> DomainResult<Vec<BookmarkImportData>> {
debug!(path = %path, "Importing bookmarks from JSON");
let file = File::open(path)
.map_err(|e| DomainError::CannotFetchMetadata(format!("Failed to open file: {}", e)))?;
let mut reader = BufReader::new(file);
let mut content = String::new();
reader.read_to_string(&mut content).map_err(|e| {
DomainError::RepositoryError(RepositoryError::Other(format!(
"Failed to read file content: {}",
e
)))
})?;
let json_bookmarks: Vec<JsonBookmark> = serde_json::from_str(&content).map_err(|e| {
DomainError::RepositoryError(RepositoryError::Other(format!(
"Failed to parse JSON: {}. Expected a JSON array of bookmark objects.",
e
)))
})?;
let mut imports = Vec::new();
for bookmark in json_bookmarks {
let mut tags = HashSet::new();
for tag_str in &bookmark.tags {
match Tag::new(tag_str) {
Ok(tag) => {
if tag.is_system_tag() && !tag.is_known_system_tag() {
warn!(tag = %tag.value(), "Unknown system tag ignored");
eprintln!(
"{} Unknown system tag '{}' ignored",
"Warning".yellow(),
tag.value()
);
continue;
}
tags.insert(tag);
}
Err(e) => {
warn!(tag = %tag_str, error = %e, "Invalid tag");
eprintln!("{} Invalid tag '{}': {}", "Warning".yellow(), tag_str, e);
}
}
}
imports.push(BookmarkImportData {
url: bookmark.url,
title: bookmark.title,
content: bookmark.description,
tags,
});
}
Ok(imports)
}
fn import_files(
&self,
_paths: &[String],
_options: &crate::domain::repositories::import_repository::ImportOptions,
) -> DomainResult<Vec<crate::domain::repositories::import_repository::FileImportData>> {
Err(DomainError::RepositoryError(
crate::domain::error::RepositoryError::Other(
"File import not supported by JsonImportRepository".to_string(),
),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn given_json_array_file_when_import_bookmarks_then_creates_bookmark_data() -> DomainResult<()>
{
let mut temp_file = NamedTempFile::new()?;
write!(
temp_file,
r#"[
{{"url": "https://example.com/test", "title": "Test Entry", "description": "Test Description", "tags": ["test", "_imported_"]}},
{{"url": "https://example.com/test2", "title": "Test Entry 2", "description": "Another Test", "tags": ["test2"]}}
]"#
).expect("Failed to write to temp file");
let repo = JsonImportRepository::new();
let imports = repo.import_json_bookmarks(temp_file.path().to_str().unwrap())?;
assert_eq!(imports.len(), 2);
assert_eq!(imports[0].url, "https://example.com/test");
assert_eq!(imports[0].title, "Test Entry");
assert_eq!(imports[0].content, "Test Description");
assert!(imports[0].tags.iter().any(|t| t.value() == "test"));
assert!(imports[0].tags.iter().any(|t| t.value() == "_imported_"));
assert_eq!(imports[1].url, "https://example.com/test2");
assert_eq!(imports[1].title, "Test Entry 2");
assert_eq!(imports[1].content, "Another Test");
assert_eq!(imports[1].tags.len(), 1);
assert!(imports[1].tags.iter().any(|t| t.value() == "test2"));
Ok(())
}
#[test]
fn given_ndjson_file_when_import_as_bookmarks_then_returns_error() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(
temp_file,
r#"{{"url": "https://example.com/test", "title": "Test Entry", "description": "Test Description", "tags": ["test"]}}"#
).expect("Failed to write to temp file");
writeln!(
temp_file,
r#"{{"url": "https://example.com/test2", "title": "Test Entry 2", "description": "Another Test", "tags": ["test2"]}}"#
).expect("Failed to write to temp file");
let repo = JsonImportRepository::new();
let result = repo.import_json_bookmarks(temp_file.path().to_str().unwrap());
assert!(result.is_err());
}
}