use super::models::{AllBoardsExport, BoardExport};
use crate::{ArchivedCard, Board, Card, Column, Snapshot, Sprint};
use std::io;
pub struct ImportedEntities {
pub boards: Vec<Board>,
pub columns: Vec<Column>,
pub cards: Vec<Card>,
pub archived_cards: Vec<ArchivedCard>,
pub sprints: Vec<Sprint>,
}
pub struct BoardImporter;
impl BoardImporter {
pub fn try_load_snapshot(json: &str) -> Option<Snapshot> {
let envelope: serde_json::Value = serde_json::from_str(json).ok()?;
let version = envelope.get("version")?.as_u64()?;
if version == 2 {
let data = envelope.get("data")?;
serde_json::from_value(data.clone()).ok()
} else {
None
}
}
pub fn import_from_json(json: &str) -> Result<AllBoardsExport, io::Error> {
if let Ok(envelope) = serde_json::from_str::<serde_json::Value>(json) {
if let Some(version) = envelope.get("version").and_then(|v| v.as_u64()) {
if version == 2 {
if let Some(data) = envelope.get("data") {
if let Ok(snapshot) = serde_json::from_value::<Snapshot>(data.clone()) {
return Ok(Self::convert_snapshot_to_export(snapshot));
}
}
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"V2 format detected but data section is invalid".to_string(),
));
}
}
}
serde_json::from_str(json).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Invalid JSON format. Expected {{\"boards\": [...]}} structure (V1) or {{\"version\": 2, \"data\": {{...}}}} structure (V2). Error: {}",
err
),
)
})
}
pub fn convert_snapshot_to_export(snapshot: Snapshot) -> AllBoardsExport {
let mut board_exports = Vec::new();
for board in snapshot.boards {
let board_columns: Vec<_> = snapshot
.columns
.iter()
.filter(|c| c.board_id == board.id)
.cloned()
.collect();
let board_cards: Vec<_> = snapshot
.cards
.iter()
.filter(|c| board_columns.iter().any(|col| col.id == c.column_id))
.cloned()
.collect();
let board_sprints: Vec<_> = snapshot
.sprints
.iter()
.filter(|s| s.board_id == board.id)
.cloned()
.collect();
let board_archived: Vec<_> = snapshot
.archived_cards
.iter()
.filter(|a| {
board_columns
.iter()
.any(|col| col.id == a.original_column_id)
})
.cloned()
.collect();
board_exports.push(BoardExport {
board,
columns: board_columns,
cards: board_cards,
sprints: board_sprints,
archived_cards: board_archived,
});
}
AllBoardsExport {
boards: board_exports,
}
}
pub fn import_from_file(filename: &str) -> io::Result<AllBoardsExport> {
let content = std::fs::read_to_string(filename)?;
Self::import_from_json(&content)
}
pub fn extract_entities(import: AllBoardsExport) -> ImportedEntities {
let mut boards = Vec::new();
let mut columns = Vec::new();
let mut cards = Vec::new();
let mut archived_cards = Vec::new();
let mut sprints = Vec::new();
for board_data in import.boards {
boards.push(board_data.board);
columns.extend(board_data.columns);
cards.extend(board_data.cards);
archived_cards.extend(board_data.archived_cards);
sprints.extend(board_data.sprints);
}
ImportedEntities {
boards,
columns,
cards,
archived_cards,
sprints,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_import_from_json_v1_valid() {
let json = r#"{
"boards": [
{
"board": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Test Board",
"description": null,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"sprint_prefix": null,
"card_prefix": null,
"task_sort_field": "Default",
"task_sort_order": "Ascending",
"active_sprint_id": null,
"sprint_duration_days": null,
"sprint_names": [],
"next_sprint_number": 1,
"sprint_name_used_count": 0,
"prefix_counters": {},
"sprint_counters": {},
"task_list_view": "Flat"
},
"columns": [],
"cards": [],
"archived_cards": [],
"sprints": []
}
]
}"#;
let result = BoardImporter::import_from_json(json);
assert!(result.is_ok());
let import = result.unwrap();
assert_eq!(import.boards.len(), 1);
assert_eq!(import.boards[0].board.name, "Test Board");
}
#[test]
fn test_import_from_json_invalid() {
let json = r#"{ "invalid": "format" }"#;
let result = BoardImporter::import_from_json(json);
assert!(result.is_err());
}
#[test]
fn test_extract_entities() {
let board = Board::new("Test".to_string(), None);
let column = Column::new(board.id, "Todo".to_string(), 0);
let mut board_mut = board.clone();
let card = Card::new(&mut board_mut, column.id, "Task".to_string(), 0, "task");
let export = AllBoardsExport {
boards: vec![BoardExport {
board: board.clone(),
columns: vec![column.clone()],
cards: vec![card.clone()],
archived_cards: vec![],
sprints: vec![],
}],
};
let entities = BoardImporter::extract_entities(export);
assert_eq!(entities.boards.len(), 1);
assert_eq!(entities.columns.len(), 1);
assert_eq!(entities.cards.len(), 1);
assert_eq!(entities.archived_cards.len(), 0);
assert_eq!(entities.sprints.len(), 0);
}
#[test]
fn test_try_load_snapshot_not_v2() {
let json = r#"{"boards": []}"#;
assert!(BoardImporter::try_load_snapshot(json).is_none());
}
#[test]
fn test_convert_snapshot_to_export() {
let board = Board::new("Test".to_string(), None);
let column = Column::new(board.id, "Todo".to_string(), 0);
let snapshot = Snapshot {
boards: vec![board.clone()],
columns: vec![column.clone()],
cards: vec![],
archived_cards: vec![],
sprints: vec![],
graph: crate::DependencyGraph::new(),
};
let export = BoardImporter::convert_snapshot_to_export(snapshot);
assert_eq!(export.boards.len(), 1);
assert_eq!(export.boards[0].board.name, "Test");
assert_eq!(export.boards[0].columns.len(), 1);
}
}