Skip to main content

omni_dev/atlassian/
api.rs

1//! Atlassian content API trait and shared types.
2//!
3//! Defines the [`AtlassianApi`] trait for abstracting over JIRA and
4//! Confluence backends, plus the [`ContentItem`] and [`ContentMetadata`]
5//! types used as the common read result.
6
7use std::future::Future;
8use std::pin::Pin;
9
10use anyhow::Result;
11
12use crate::atlassian::adf::AdfDocument;
13
14/// A content item fetched from an Atlassian Cloud API.
15#[derive(Debug, Clone)]
16pub struct ContentItem {
17    /// Identifier: JIRA issue key (e.g., "PROJ-123") or Confluence page ID.
18    pub id: String,
19
20    /// Title (JIRA summary or Confluence page title).
21    pub title: String,
22
23    /// Body as raw ADF JSON value (may be `None` when the field is null).
24    pub body_adf: Option<serde_json::Value>,
25
26    /// Backend-specific metadata that maps to frontmatter fields.
27    pub metadata: ContentMetadata,
28}
29
30/// Backend-specific metadata for a content item.
31#[derive(Debug, Clone)]
32pub enum ContentMetadata {
33    /// JIRA issue metadata.
34    Jira {
35        /// Issue status name.
36        status: Option<String>,
37        /// Issue type name (Bug, Story, Task, etc.).
38        issue_type: Option<String>,
39        /// Assignee display name.
40        assignee: Option<String>,
41        /// Priority name.
42        priority: Option<String>,
43        /// Labels.
44        labels: Vec<String>,
45    },
46    /// Confluence page metadata.
47    Confluence {
48        /// Space key (e.g., "ENG").
49        space_key: String,
50        /// Page status ("current" or "draft").
51        status: Option<String>,
52        /// Page version number.
53        version: Option<u32>,
54        /// Parent page ID.
55        parent_id: Option<String>,
56    },
57}
58
59/// Trait for Atlassian content backends.
60///
61/// Follows the project's `AiClient` pattern: `Send + Sync` bounds with
62/// boxed futures for async trait methods.
63pub trait AtlassianApi: Send + Sync {
64    /// Fetches a content item by its identifier.
65    fn get_content<'a>(
66        &'a self,
67        id: &'a str,
68    ) -> Pin<Box<dyn Future<Output = Result<ContentItem>> + Send + 'a>>;
69
70    /// Updates a content item's body and optionally its title.
71    fn update_content<'a>(
72        &'a self,
73        id: &'a str,
74        body_adf: &'a AdfDocument,
75        title: Option<&'a str>,
76    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
77
78    /// Verifies authentication and returns a display name.
79    fn verify_auth<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
80
81    /// Returns the backend type name ("jira" or "confluence").
82    fn backend_name(&self) -> &'static str;
83}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used, clippy::expect_used)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn content_metadata_jira_variant() {
92        let meta = ContentMetadata::Jira {
93            status: Some("Open".to_string()),
94            issue_type: Some("Bug".to_string()),
95            assignee: None,
96            priority: Some("High".to_string()),
97            labels: vec!["backend".to_string()],
98        };
99        match &meta {
100            ContentMetadata::Jira { status, labels, .. } => {
101                assert_eq!(status.as_deref(), Some("Open"));
102                assert_eq!(labels.len(), 1);
103            }
104            _ => panic!("Expected Jira variant"),
105        }
106    }
107
108    #[test]
109    fn content_metadata_confluence_variant() {
110        let meta = ContentMetadata::Confluence {
111            space_key: "ENG".to_string(),
112            status: Some("current".to_string()),
113            version: Some(7),
114            parent_id: None,
115        };
116        match &meta {
117            ContentMetadata::Confluence {
118                space_key, version, ..
119            } => {
120                assert_eq!(space_key, "ENG");
121                assert_eq!(*version, Some(7));
122            }
123            _ => panic!("Expected Confluence variant"),
124        }
125    }
126
127    #[test]
128    fn content_item_fields() {
129        let item = ContentItem {
130            id: "PROJ-123".to_string(),
131            title: "Fix the bug".to_string(),
132            body_adf: None,
133            metadata: ContentMetadata::Jira {
134                status: None,
135                issue_type: None,
136                assignee: None,
137                priority: None,
138                labels: vec![],
139            },
140        };
141        assert_eq!(item.id, "PROJ-123");
142        assert_eq!(item.title, "Fix the bug");
143        assert!(item.body_adf.is_none());
144    }
145}