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