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_validated::ValidatedAdfDocument;
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    ///
72    /// `body_adf` is a [`ValidatedAdfDocument`] so the type system enforces
73    /// that callers ran nesting validation before reaching the wire.
74    fn update_content<'a>(
75        &'a self,
76        id: &'a str,
77        body_adf: &'a ValidatedAdfDocument,
78        title: Option<&'a str>,
79    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
80
81    /// Verifies authentication and returns a display name.
82    fn verify_auth<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
83
84    /// Returns the backend type name ("jira" or "confluence").
85    fn backend_name(&self) -> &'static str;
86}
87
88#[cfg(test)]
89#[allow(
90    clippy::unwrap_used,
91    clippy::expect_used,
92    clippy::match_wildcard_for_single_variants
93)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn content_metadata_jira_variant() {
99        let meta = ContentMetadata::Jira {
100            status: Some("Open".to_string()),
101            issue_type: Some("Bug".to_string()),
102            assignee: None,
103            priority: Some("High".to_string()),
104            labels: vec!["backend".to_string()],
105        };
106        match &meta {
107            ContentMetadata::Jira { status, labels, .. } => {
108                assert_eq!(status.as_deref(), Some("Open"));
109                assert_eq!(labels.len(), 1);
110            }
111            _ => panic!("Expected Jira variant"),
112        }
113    }
114
115    #[test]
116    fn content_metadata_confluence_variant() {
117        let meta = ContentMetadata::Confluence {
118            space_key: "ENG".to_string(),
119            status: Some("current".to_string()),
120            version: Some(7),
121            parent_id: None,
122        };
123        match &meta {
124            ContentMetadata::Confluence {
125                space_key, version, ..
126            } => {
127                assert_eq!(space_key, "ENG");
128                assert_eq!(*version, Some(7));
129            }
130            _ => panic!("Expected Confluence variant"),
131        }
132    }
133
134    #[test]
135    fn content_item_fields() {
136        let item = ContentItem {
137            id: "PROJ-123".to_string(),
138            title: "Fix the bug".to_string(),
139            body_adf: None,
140            metadata: ContentMetadata::Jira {
141                status: None,
142                issue_type: None,
143                assignee: None,
144                priority: None,
145                labels: vec![],
146            },
147        };
148        assert_eq!(item.id, "PROJ-123");
149        assert_eq!(item.title, "Fix the bug");
150        assert!(item.body_adf.is_none());
151    }
152}