use anyhow::{bail, Result};
use design::index::DocumentIndex;
use oxur_cli::table::TableStyleConfig;
use std::env;
use tabled::{builder::Builder, Tabled};
#[derive(Tabled)]
struct DocumentInfoRow {
field: String,
content: String,
}
fn get_relative_path(doc_path: &std::path::Path) -> String {
let current_dir = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
match doc_path.strip_prefix(¤t_dir) {
Ok(rel_path) => rel_path.to_string_lossy().to_string(),
Err(_) => doc_path.to_string_lossy().to_string(),
}
}
pub fn show_document(index: &DocumentIndex, number: u32, _metadata_only: bool) -> Result<()> {
let doc = match index.get(number) {
Some(d) => d,
None => bail!("Document {:04} not found", number),
};
let mut builder = Builder::default();
builder.push_record(["DOCUMENT", "INFORMATION"]);
builder.push_record(["Field", "Content"]);
builder.push_record([" Number", &format!(" {:04}", doc.metadata.number)]);
builder.push_record([" Title", &format!(" {} ", doc.metadata.title)]);
builder.push_record([" Author", &format!(" {}", doc.metadata.author)]);
builder.push_record([" State", &format!(" {}", doc.metadata.state.as_str())]);
builder.push_record([" Created ", &format!(" {}", doc.metadata.created)]);
builder.push_record([" Updated ", &format!(" {}", doc.metadata.updated)]);
if let Some(component) = &doc.metadata.component {
builder.push_record([" Component", &format!(" {}", component)]);
}
if !doc.metadata.tags.is_empty() {
let tags_str = doc.metadata.tags.join(", ");
builder.push_record([" Tags", &format!(" {}", tags_str)]);
}
builder.push_record([" Version", &format!(" {}", doc.metadata.version)]);
let rel_path = get_relative_path(&doc.path);
builder.push_record([" Path", &format!(" {} ", rel_path)]);
if let Some(supersedes) = doc.metadata.supersedes {
builder.push_record([" Supersedes", &format!(" {:04}", supersedes)]);
}
if let Some(superseded_by) = doc.metadata.superseded_by {
builder.push_record([" Superseded By", &format!(" {:04}", superseded_by)]);
}
builder.push_record(["", ""]);
let mut table = builder.build();
let config = TableStyleConfig::default();
config.apply_to_table::<DocumentInfoRow>(&mut table);
println!();
println!("{}", table);
println!();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
use design::doc::{DocMetadata, DocState};
use design::index::DocumentIndex;
use design::state::{DocumentRecord, DocumentState};
use tempfile::TempDir;
fn create_test_index_with_docs() -> DocumentIndex {
let temp = TempDir::new().unwrap();
let mut state = DocumentState::new();
let meta1 = DocMetadata {
number: 1,
title: "First Document".to_string(),
author: "Alice".to_string(),
component: None,
tags: Vec::new(),
created: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
updated: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
state: DocState::Draft,
supersedes: None,
superseded_by: None,
version: "1.0".to_string(),
};
state.upsert(
1,
DocumentRecord {
metadata: meta1,
path: "0001-first.md".to_string(),
checksum: "abc123".to_string(),
file_size: 100,
modified: chrono::Utc::now(),
},
);
let meta2 = DocMetadata {
number: 2,
title: "Second Document".to_string(),
author: "Bob".to_string(),
component: None,
tags: Vec::new(),
created: NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
updated: NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
state: DocState::Active,
supersedes: Some(1),
superseded_by: None,
version: "1.0".to_string(),
};
state.upsert(
2,
DocumentRecord {
metadata: meta2,
path: "0002-second.md".to_string(),
checksum: "def456".to_string(),
file_size: 150,
modified: chrono::Utc::now(),
},
);
let meta3 = DocMetadata {
number: 3,
title: "Third Document".to_string(),
author: "Charlie".to_string(),
component: None,
tags: Vec::new(),
created: NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(),
updated: NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
state: DocState::Superseded,
supersedes: None,
superseded_by: Some(4),
version: "1.0".to_string(),
};
state.upsert(
3,
DocumentRecord {
metadata: meta3,
path: "0003-third.md".to_string(),
checksum: "ghi789".to_string(),
file_size: 200,
modified: chrono::Utc::now(),
},
);
DocumentIndex::from_state(&state, temp.path()).unwrap()
}
#[test]
fn test_show_existing_document() {
let index = create_test_index_with_docs();
let result = show_document(&index, 1, false);
assert!(result.is_ok());
}
#[test]
fn test_show_nonexistent_document() {
let index = create_test_index_with_docs();
let result = show_document(&index, 9999, false);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
fn test_show_metadata_only() {
let index = create_test_index_with_docs();
let result = show_document(&index, 1, true);
assert!(result.is_ok());
}
#[test]
fn test_show_document_with_supersedes() {
let index = create_test_index_with_docs();
let result = show_document(&index, 2, false);
assert!(result.is_ok());
}
#[test]
fn test_show_document_with_superseded_by() {
let index = create_test_index_with_docs();
let result = show_document(&index, 3, false);
assert!(result.is_ok());
}
#[test]
fn test_show_all_documents() {
let index = create_test_index_with_docs();
for num in [1, 2, 3] {
let result = show_document(&index, num, false);
assert!(result.is_ok(), "Failed to show document {}", num);
}
}
#[test]
fn test_show_empty_index() {
let temp = TempDir::new().unwrap();
let index = DocumentIndex::new(temp.path()).unwrap();
let result = show_document(&index, 1, false);
assert!(result.is_err());
}
#[test]
fn test_get_relative_path_strips_prefix() {
let doc_path = std::path::PathBuf::from("some/nested/path.md");
let result = get_relative_path(&doc_path);
assert!(result == "some/nested/path.md" || result.ends_with("some/nested/path.md"));
}
#[test]
fn test_get_relative_path_returns_full_path() {
let doc_path = std::path::PathBuf::from("/some/absolute/path/not/under/cwd/doc.md");
let result = get_relative_path(&doc_path);
assert_eq!(result, "/some/absolute/path/not/under/cwd/doc.md");
}
}