use std::io;
use io::Read;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "enable-graphql", derive(async_graphql::SimpleObject))]
pub struct Page {
#[serde(rename = "title")]
pub title: Option<String>,
#[serde(rename = "ns")]
pub namespace: Option<String>,
#[serde(rename = "id")]
pub id: Option<String>,
#[serde(rename = "redirect")]
pub redirect: Option<Redirect>,
#[serde(rename = "restrictions")]
pub restrictions: Option<String>,
#[serde(rename = "revision")]
pub revision: Option<Revision>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "enable-graphql", derive(async_graphql::SimpleObject))]
pub struct Redirect {
#[serde(rename = "@title")]
pub title: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "enable-graphql", derive(async_graphql::SimpleObject))]
pub struct Revision {
#[serde(rename = "id")]
pub id: Option<String>,
#[serde(rename = "parentid")]
pub parent_id: Option<String>,
#[serde(rename = "timestamp")]
pub timestamp: Option<String>,
#[serde(rename = "comment")]
pub comment: Option<String>,
#[serde(rename = "origin")]
pub origin: Option<String>,
#[serde(rename = "model")]
pub model: Option<String>,
#[serde(rename = "format")]
pub format: Option<String>,
#[serde(rename = "text")]
pub text: Option<String>,
#[serde(rename = "sha1")]
pub sha1: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename = "container")]
pub struct Container {
#[serde(rename = "page")]
pub pages: Vec<Page>,
}
pub const CONTAINER_PREFIX: &str = "<container>";
pub const CONTAINER_SUFFIX: &str = "</container>";
impl Container {
pub fn from_pages_str(s: &str) -> Result<Self, io::Error> {
let ps = Read::chain(CONTAINER_PREFIX.as_bytes(), s.as_bytes());
let pss = Read::chain(ps, CONTAINER_SUFFIX.as_bytes());
quick_xml::de::from_reader(pss).map_err(io::Error::other)
}
pub fn into_pages(self) -> Vec<Page> {
self.pages
}
}
pub fn xmlpages2pages(xpages: &str) -> Result<Vec<Page>, io::Error> {
Container::from_pages_str(xpages).map(|c| c.into_pages())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
fn make_container(input: &str) -> io::Result<Container> {
Container::from_pages_str(input)
}
#[test]
fn test_container_parsing_single_page() {
let xml = r#"
<page>
<title>Test Page</title>
<id>42</id>
<restrictions>none</restrictions>
<revision>
<id>1000</id>
<timestamp>2025-08-10T12:34:56Z</timestamp>
<minor/>
<comment>Initial revision</comment>
<text>Page content goes here.</text>
</revision>
</page>
"#;
let container = make_container(xml).expect("Failed to parse container");
assert_eq!(container.pages.len(), 1);
let page = &container.pages[0];
assert_eq!(page.title.as_deref(), Some("Test Page"));
assert_eq!(page.id.as_deref(), Some("42"));
assert_eq!(page.restrictions.as_deref(), Some("none"));
let rev = page.revision.as_ref().expect("revision missing");
assert_eq!(rev.id.as_deref(), Some("1000"));
assert_eq!(rev.timestamp.as_deref(), Some("2025-08-10T12:34:56Z"));
assert_eq!(rev.comment.as_deref(), Some("Initial revision"));
assert_eq!(rev.text.as_deref(), Some("Page content goes here."));
}
#[test]
fn test_container_parsing_multiple_pages() {
let xml = r#"
<page>
<title>First</title>
<id>1</id>
<revision><id>10</id><timestamp>2025-01-01T00:00:00Z</timestamp></revision>
</page>
<page>
<title>Second</title>
<id>2</id>
<revision><id>20</id><timestamp>2025-02-01T00:00:00Z</timestamp></revision>
</page>
"#;
let container = make_container(xml).expect("Failed to parse container");
assert_eq!(container.pages.len(), 2);
let p1 = &container.pages[0];
assert_eq!(p1.title.as_deref(), Some("First"));
assert_eq!(p1.id.as_deref(), Some("1"));
let r1 = p1.revision.as_ref().expect("revision missing");
assert_eq!(r1.id.as_deref(), Some("10"));
assert_eq!(r1.timestamp.as_deref(), Some("2025-01-01T00:00:00Z"));
let p2 = &container.pages[1];
assert_eq!(p2.title.as_deref(), Some("Second"));
assert_eq!(p2.id.as_deref(), Some("2"));
let r2 = p2.revision.as_ref().expect("revision missing");
assert_eq!(r2.id.as_deref(), Some("20"));
assert_eq!(r2.timestamp.as_deref(), Some("2025-02-01T00:00:00Z"));
}
#[test]
fn test_container_parsing_error() {
let bad_xml = r#"<page><title>Oops</title></page><invalid"#;
assert!(make_container(bad_xml).is_err());
}
}