use project_record::validate_record_id; use serde::{Deserialize, Serialize}; use crate::{Error, Result}; const SUMMARY_BODY_LIMIT: usize = 240; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ProjectRecordList { pub items: Vec, pub invalid_records: Vec, pub record_authority: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct InvalidProjectRecord { pub label: String, pub reason: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TicketSummary { pub id: String, pub title: String, pub state: String, pub priority: String, pub updated_at: Option, pub queued_by: Option, pub queued_at: Option, pub workspace_action_priority: String, pub record_source: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TicketDetail { pub id: String, pub title: String, pub state: String, pub priority: String, pub created_at: Option, pub updated_at: Option, pub queued_by: Option, pub queued_at: Option, pub risk_flags: Vec, pub body: String, pub body_truncated: bool, pub event_count: usize, pub artifact_count: usize, pub record_source: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ObjectiveSummary { pub id: String, pub title: String, pub state: String, pub updated_at: Option, pub summary: String, pub linked_tickets: Vec, pub record_source: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ObjectiveDetail { pub id: String, pub title: String, pub state: String, pub created_at: Option, pub updated_at: Option, pub linked_tickets: Vec, pub resources: Vec, pub body: String, pub body_truncated: bool, pub record_source: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ObjectiveResourceSummary { pub path: String, pub media_type: Option, pub bytes: usize, pub updated_at: String, } pub(crate) fn validate_project_id(id: &str) -> Result<()> { validate_record_id(id).map_err(|_| Error::InvalidRecordId(id.to_string())) } pub(crate) fn summarize_body(body: &str) -> String { let summary = body .lines() .map(str::trim) .find(|line| !line.is_empty() && !line.starts_with('#')) .unwrap_or_default(); let (summary, truncated) = truncate_body(summary, SUMMARY_BODY_LIMIT); if truncated { format!("{summary}…") } else { summary } } pub(crate) fn truncate_body(body: &str, limit: usize) -> (String, bool) { if body.len() <= limit { return (body.to_string(), false); } let mut end = limit; while !body.is_char_boundary(end) { end -= 1; } (body[..end].to_string(), true) }