use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct ConfluenceSpace {
pub id: String,
pub key: String,
pub name: String,
#[serde(rename = "type", default)]
pub space_type: String,
#[serde(default)]
pub status: String,
pub description: Option<SpaceDescription>,
#[serde(rename = "_links")]
pub links: Option<SpaceLinks>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SpaceDescription {
pub plain: Option<PlainValue>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PlainValue {
pub value: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SpaceLinks {
pub webui: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SpaceListResponse {
pub results: Vec<ConfluenceSpace>,
#[serde(rename = "_links")]
pub links: Option<PaginationLinks>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfluencePage {
pub id: String,
#[serde(default)]
pub status: String,
pub title: String,
pub space_id: Option<String>,
pub parent_id: Option<String>,
pub version: Option<PageVersion>,
pub body: Option<PageBody>,
#[serde(rename = "_links")]
pub links: Option<PageLinks>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PageVersion {
pub number: i64,
pub message: Option<String>,
pub created_at: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PageBody {
pub storage: Option<StorageBody>,
pub view: Option<StorageBody>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct StorageBody {
pub value: String,
pub representation: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PageLinks {
pub webui: Option<String>,
pub editui: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PageListResponse {
pub results: Vec<ConfluencePage>,
#[serde(rename = "_links")]
pub links: Option<PaginationLinks>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ConfluencePageV1 {
pub id: String,
#[serde(rename = "type", default)]
pub page_type: String,
#[serde(default)]
pub status: String,
pub title: String,
pub space: Option<SpaceRef>,
pub version: Option<PageVersionV1>,
pub body: Option<PageBody>,
pub ancestors: Option<Vec<AncestorRef>>,
#[serde(rename = "_links")]
pub links: Option<PageLinksV1>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SpaceRef {
pub key: String,
pub name: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PageVersionV1 {
pub number: i64,
pub when: Option<String>,
pub by: Option<VersionAuthor>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VersionAuthor {
pub display_name: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AncestorRef {
pub id: String,
pub title: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PageLinksV1 {
pub webui: Option<String>,
pub edit: Option<String>,
#[serde(rename = "self")]
pub self_link: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PageListResponseV1 {
pub results: Vec<ConfluencePageV1>,
#[serde(rename = "_links")]
pub links: Option<PaginationLinks>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchResult {
pub results: Vec<SearchResultItem>,
pub total_size: Option<i64>,
#[serde(rename = "_links")]
pub links: Option<PaginationLinks>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchResultItem {
pub content: Option<ConfluencePageV1>,
pub title: String,
pub excerpt: Option<String>,
pub url: Option<String>,
pub result_global_container: Option<GlobalContainer>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GlobalContainer {
pub title: String,
pub display_url: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct CreatePageResponse {
pub id: String,
pub title: String,
#[serde(default)]
pub status: String,
#[serde(rename = "_links")]
pub links: Option<PageLinks>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ConfluenceLabel {
pub prefix: Option<String>,
pub name: String,
pub id: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LabelListResponse {
pub results: Vec<ConfluenceLabel>,
#[serde(rename = "_links")]
pub links: Option<PaginationLinks>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfluenceAttachment {
pub id: String,
#[serde(default)]
pub status: String,
pub title: String,
pub media_type: Option<String>,
pub file_size: Option<u64>,
pub download_link: Option<String>,
#[serde(rename = "_links")]
pub links: Option<AttachmentLinks>,
pub extensions: Option<AttachmentExtensions>,
pub metadata: Option<AttachmentMetadata>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AttachmentLinks {
pub webui: Option<String>,
pub download: Option<String>,
#[serde(rename = "self")]
pub self_link: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AttachmentExtensions {
pub media_type: Option<String>,
pub file_size: Option<u64>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AttachmentMetadata {
pub media_type: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AttachmentListResponse {
pub results: Vec<ConfluenceAttachment>,
#[serde(rename = "_links")]
pub links: Option<AttachmentListLinks>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AttachmentListLinks {
pub next: Option<String>,
pub base: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PaginationLinks {
pub next: Option<String>,
}
pub enum AnyPage {
V1(ConfluencePageV1),
V2(ConfluencePage),
}
impl AnyPage {
pub fn id(&self) -> &str {
match self {
AnyPage::V1(p) => &p.id,
AnyPage::V2(p) => &p.id,
}
}
pub fn title(&self) -> &str {
match self {
AnyPage::V1(p) => &p.title,
AnyPage::V2(p) => &p.title,
}
}
pub fn status(&self) -> &str {
match self {
AnyPage::V1(p) => &p.status,
AnyPage::V2(p) => &p.status,
}
}
pub fn version_number(&self) -> i64 {
match self {
AnyPage::V1(p) => p.version.as_ref().map_or(1, |v| v.number),
AnyPage::V2(p) => p.version.as_ref().map_or(1, |v| v.number),
}
}
pub fn storage_value(&self) -> &str {
match self {
AnyPage::V1(p) => p
.body
.as_ref()
.and_then(|b| b.storage.as_ref())
.map_or("", |s| &s.value),
AnyPage::V2(p) => p
.body
.as_ref()
.and_then(|b| b.storage.as_ref())
.map_or("", |s| &s.value),
}
}
pub fn webui_link(&self) -> Option<&str> {
match self {
AnyPage::V1(p) => p.links.as_ref().and_then(|l| l.webui.as_deref()),
AnyPage::V2(p) => p.links.as_ref().and_then(|l| l.webui.as_deref()),
}
}
pub fn space_info(&self) -> Option<(&str, &str)> {
match self {
AnyPage::V1(p) => p.space.as_ref().map(|s| (s.key.as_str(), s.name.as_str())),
AnyPage::V2(_) => None,
}
}
pub fn space_id(&self) -> Option<&str> {
match self {
AnyPage::V1(_) => None,
AnyPage::V2(p) => p.space_id.as_deref(),
}
}
pub fn version_date(&self) -> Option<&str> {
match self {
AnyPage::V1(p) => p.version.as_ref().and_then(|v| v.when.as_deref()),
AnyPage::V2(p) => p.version.as_ref().and_then(|v| v.created_at.as_deref()),
}
}
pub fn version_author(&self) -> Option<&str> {
match self {
AnyPage::V1(p) => p
.version
.as_ref()
.and_then(|v| v.by.as_ref())
.map(|a| a.display_name.as_str()),
AnyPage::V2(_) => None,
}
}
pub fn ancestors(&self) -> Option<&[AncestorRef]> {
match self {
AnyPage::V1(p) => p.ancestors.as_deref(),
AnyPage::V2(_) => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_v1_page() -> ConfluencePageV1 {
ConfluencePageV1 {
id: "100".to_string(),
page_type: "page".to_string(),
status: "current".to_string(),
title: "V1 Page".to_string(),
space: Some(SpaceRef {
key: "SP".to_string(),
name: "Space Name".to_string(),
}),
version: Some(PageVersionV1 {
number: 7,
when: Some("2024-03-15T10:00:00Z".to_string()),
by: Some(VersionAuthor {
display_name: "John".to_string(),
}),
}),
body: Some(PageBody {
storage: Some(StorageBody {
value: "<p>Content</p>".to_string(),
representation: Some("storage".to_string()),
}),
view: None,
}),
ancestors: Some(vec![
AncestorRef { id: "1".to_string(), title: "Root".to_string() },
AncestorRef { id: "2".to_string(), title: "Parent".to_string() },
]),
links: Some(PageLinksV1 {
webui: Some("/pages/100".to_string()),
edit: None,
self_link: None,
}),
}
}
fn make_v2_page() -> ConfluencePage {
ConfluencePage {
id: "200".to_string(),
status: "draft".to_string(),
title: "V2 Page".to_string(),
space_id: Some("space-42".to_string()),
parent_id: Some("parent-1".to_string()),
version: Some(PageVersion {
number: 3,
message: Some("Updated".to_string()),
created_at: Some("2024-06-01T12:00:00Z".to_string()),
}),
body: Some(PageBody {
storage: Some(StorageBody {
value: "<p>V2 Body</p>".to_string(),
representation: None,
}),
view: None,
}),
links: Some(PageLinks {
webui: Some("/wiki/pages/200".to_string()),
editui: None,
}),
}
}
#[test]
fn any_page_v1_accessors() {
let page = AnyPage::V1(make_v1_page());
assert_eq!(page.id(), "100");
assert_eq!(page.title(), "V1 Page");
assert_eq!(page.status(), "current");
assert_eq!(page.version_number(), 7);
assert_eq!(page.storage_value(), "<p>Content</p>");
assert_eq!(page.webui_link(), Some("/pages/100"));
assert_eq!(page.space_info(), Some(("SP", "Space Name")));
assert_eq!(page.space_id(), None);
assert_eq!(page.version_date(), Some("2024-03-15T10:00:00Z"));
assert_eq!(page.version_author(), Some("John"));
assert_eq!(page.ancestors().unwrap().len(), 2);
}
#[test]
fn any_page_v2_accessors() {
let page = AnyPage::V2(make_v2_page());
assert_eq!(page.id(), "200");
assert_eq!(page.title(), "V2 Page");
assert_eq!(page.status(), "draft");
assert_eq!(page.version_number(), 3);
assert_eq!(page.storage_value(), "<p>V2 Body</p>");
assert_eq!(page.webui_link(), Some("/wiki/pages/200"));
assert_eq!(page.space_info(), None);
assert_eq!(page.space_id(), Some("space-42"));
assert_eq!(page.version_date(), Some("2024-06-01T12:00:00Z"));
assert_eq!(page.version_author(), None);
assert!(page.ancestors().is_none());
}
#[test]
fn any_page_v1_no_version() {
let mut page = make_v1_page();
page.version = None;
let any = AnyPage::V1(page);
assert_eq!(any.version_number(), 1); assert_eq!(any.version_date(), None);
assert_eq!(any.version_author(), None);
}
#[test]
fn any_page_v2_no_version() {
let mut page = make_v2_page();
page.version = None;
let any = AnyPage::V2(page);
assert_eq!(any.version_number(), 1);
assert_eq!(any.version_date(), None);
}
#[test]
fn any_page_v1_no_body() {
let mut page = make_v1_page();
page.body = None;
let any = AnyPage::V1(page);
assert_eq!(any.storage_value(), "");
}
#[test]
fn any_page_v2_no_body() {
let mut page = make_v2_page();
page.body = None;
let any = AnyPage::V2(page);
assert_eq!(any.storage_value(), "");
}
#[test]
fn any_page_v1_no_links() {
let mut page = make_v1_page();
page.links = None;
let any = AnyPage::V1(page);
assert_eq!(any.webui_link(), None);
}
#[test]
fn any_page_v1_no_space() {
let mut page = make_v1_page();
page.space = None;
let any = AnyPage::V1(page);
assert_eq!(any.space_info(), None);
}
#[test]
fn any_page_v1_no_ancestors() {
let mut page = make_v1_page();
page.ancestors = None;
let any = AnyPage::V1(page);
assert!(any.ancestors().is_none());
}
#[test]
fn confluence_page_v2_deserialize() {
let json = r#"{
"id": "1",
"status": "current",
"title": "Test",
"spaceId": "sp1",
"version": {"number": 2, "createdAt": "2024-01-01"}
}"#;
let page: ConfluencePage = serde_json::from_str(json).unwrap();
assert_eq!(page.id, "1");
assert_eq!(page.title, "Test");
assert_eq!(page.space_id, Some("sp1".to_string()));
assert_eq!(page.version.unwrap().number, 2);
}
#[test]
fn confluence_page_v1_deserialize() {
let json = r#"{
"id": "2",
"type": "page",
"status": "current",
"title": "V1 Test",
"space": {"key": "DEV", "name": "Dev Space"},
"version": {"number": 3}
}"#;
let page: ConfluencePageV1 = serde_json::from_str(json).unwrap();
assert_eq!(page.id, "2");
assert_eq!(page.page_type, "page");
assert_eq!(page.space.unwrap().key, "DEV");
}
#[test]
fn confluence_label_deserialize() {
let json = r#"{"name": "important", "prefix": "global", "id": "10"}"#;
let label: ConfluenceLabel = serde_json::from_str(json).unwrap();
assert_eq!(label.name, "important");
assert_eq!(label.prefix, Some("global".to_string()));
}
#[test]
fn confluence_attachment_deserialize() {
let json = r#"{
"id": "att1",
"status": "current",
"title": "file.pdf",
"mediaType": "application/pdf",
"fileSize": 12345
}"#;
let att: ConfluenceAttachment = serde_json::from_str(json).unwrap();
assert_eq!(att.title, "file.pdf");
assert_eq!(att.file_size, Some(12345));
}
#[test]
fn search_result_deserialize() {
let json = r#"{
"results": [{"title": "Found page", "excerpt": "...match..."}],
"totalSize": 1
}"#;
let sr: SearchResult = serde_json::from_str(json).unwrap();
assert_eq!(sr.results.len(), 1);
assert_eq!(sr.total_size, Some(1));
}
#[test]
fn space_list_response_deserialize() {
let json = r#"{
"results": [{"id": "1", "key": "DEV", "name": "Dev"}]
}"#;
let resp: SpaceListResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.results.len(), 1);
assert_eq!(resp.results[0].key, "DEV");
}
}