use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Frontmatter {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_number: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<u32>,
pub created_at: String,
pub updated_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub projects: Option<Vec<String>>,
#[serde(flatten)]
pub custom_fields: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Item {
pub id: String,
pub title: String,
pub body: String,
pub frontmatter: Frontmatter,
pub comment: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CreateOptions {
pub title: String,
pub body: String,
pub id: Option<String>,
pub status: Option<String>,
pub priority: Option<u32>,
pub tags: Option<Vec<String>>,
pub projects: Option<Vec<String>>,
pub custom_fields: HashMap<String, serde_json::Value>,
pub comment: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DuplicateOptions {
pub source_dir: PathBuf,
pub target_dir: PathBuf,
pub item_id: String,
pub new_id: Option<String>,
pub new_title: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DuplicateResult {
pub item: Item,
pub original_id: String,
}
#[derive(Debug, Clone, Default)]
pub struct UpdateOptions {
pub title: Option<String>,
pub body: Option<String>,
pub status: Option<String>,
pub priority: Option<u32>,
pub tags: Option<Vec<String>>,
pub projects: Option<Vec<String>>,
pub custom_fields: HashMap<String, serde_json::Value>,
pub comment: Option<String>,
}
#[derive(Debug, Clone)]
pub struct MoveOptions {
pub source_dir: PathBuf,
pub target_dir: PathBuf,
pub item_id: String,
pub new_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct MoveResult {
pub item: Item,
pub old_id: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frontmatter_minimal_serialization() {
let fm = Frontmatter {
display_number: None,
status: None,
priority: None,
created_at: "2024-01-01T00:00:00Z".to_string(),
updated_at: "2024-01-01T00:00:00Z".to_string(),
deleted_at: None,
tags: None,
projects: None,
custom_fields: HashMap::new(),
};
let yaml = serde_yaml::to_string(&fm).unwrap();
assert!(!yaml.contains("displayNumber"));
assert!(!yaml.contains("status"));
assert!(!yaml.contains("priority"));
assert!(!yaml.contains("deletedAt"));
assert!(!yaml.contains("tags"));
assert!(!yaml.contains("customFields"));
assert!(!yaml.contains("env:"));
assert!(yaml.contains("createdAt"));
assert!(yaml.contains("updatedAt"));
}
#[test]
fn test_frontmatter_full_serialization() {
let fm = Frontmatter {
display_number: Some(42),
status: Some("open".to_string()),
priority: Some(2),
created_at: "2024-01-01T00:00:00Z".to_string(),
updated_at: "2024-01-02T00:00:00Z".to_string(),
deleted_at: Some("2024-01-03T00:00:00Z".to_string()),
tags: Some(vec!["bug".to_string(), "frontend".to_string()]),
projects: Some(vec!["frontend".to_string()]),
custom_fields: HashMap::from([("env".to_string(), serde_json::json!("prod"))]),
};
let yaml = serde_yaml::to_string(&fm).unwrap();
assert!(yaml.contains("displayNumber: 42"));
assert!(yaml.contains("status: open"));
assert!(yaml.contains("priority: 2"));
assert!(yaml.contains("deletedAt"));
assert!(yaml.contains("- bug"));
assert!(yaml.contains("- frontend"));
assert!(!yaml.contains("customFields:"));
assert!(yaml.contains("env: prod"));
}
#[test]
fn test_frontmatter_roundtrip() {
let fm = Frontmatter {
display_number: Some(1),
status: Some("closed".to_string()),
priority: Some(3),
created_at: "2024-06-15T12:00:00Z".to_string(),
updated_at: "2024-06-15T13:00:00Z".to_string(),
deleted_at: None,
tags: Some(vec!["urgent".to_string()]),
projects: None,
custom_fields: HashMap::new(),
};
let yaml = serde_yaml::to_string(&fm).unwrap();
let parsed: Frontmatter = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(fm, parsed);
}
#[test]
fn test_create_options() {
let opts = CreateOptions {
title: "Test".to_string(),
body: "Body".to_string(),
id: None,
status: Some("open".to_string()),
priority: Some(2),
tags: None,
projects: None,
custom_fields: HashMap::new(),
comment: None,
};
assert_eq!(opts.title, "Test");
assert!(opts.id.is_none());
}
#[test]
fn test_update_options_default() {
let opts = UpdateOptions::default();
assert!(opts.title.is_none());
assert!(opts.body.is_none());
assert!(opts.status.is_none());
assert!(opts.priority.is_none());
assert!(opts.tags.is_none());
assert!(opts.custom_fields.is_empty());
}
}