Skip to main content

rs_wikipages2struct/
lib.rs

1use std::io;
2
3use io::Read;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8#[cfg_attr(feature = "enable-graphql", derive(async_graphql::SimpleObject))]
9pub struct Page {
10    #[serde(rename = "title")]
11    pub title: Option<String>,
12
13    #[serde(rename = "ns")]
14    pub namespace: Option<String>,
15
16    #[serde(rename = "id")]
17    pub id: Option<String>,
18
19    #[serde(rename = "redirect")]
20    pub redirect: Option<Redirect>,
21
22    #[serde(rename = "restrictions")]
23    pub restrictions: Option<String>,
24
25    #[serde(rename = "revision")]
26    pub revision: Option<Revision>,
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30#[cfg_attr(feature = "enable-graphql", derive(async_graphql::SimpleObject))]
31pub struct Redirect {
32    #[serde(rename = "@title")]
33    pub title: Option<String>,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[cfg_attr(feature = "enable-graphql", derive(async_graphql::SimpleObject))]
38pub struct Revision {
39    #[serde(rename = "id")]
40    pub id: Option<String>,
41
42    #[serde(rename = "parentid")]
43    pub parent_id: Option<String>,
44
45    #[serde(rename = "timestamp")]
46    pub timestamp: Option<String>,
47
48    #[serde(rename = "comment")]
49    pub comment: Option<String>,
50
51    #[serde(rename = "origin")]
52    pub origin: Option<String>,
53
54    #[serde(rename = "model")]
55    pub model: Option<String>,
56
57    #[serde(rename = "format")]
58    pub format: Option<String>,
59
60    #[serde(rename = "text")]
61    pub text: Option<String>,
62
63    #[serde(rename = "sha1")]
64    pub sha1: Option<String>,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(rename = "container")]
69pub struct Container {
70    #[serde(rename = "page")]
71    pub pages: Vec<Page>,
72}
73
74pub const CONTAINER_PREFIX: &str = "<container>";
75pub const CONTAINER_SUFFIX: &str = "</container>";
76
77impl Container {
78    pub fn from_pages_str(s: &str) -> Result<Self, io::Error> {
79        let ps = Read::chain(CONTAINER_PREFIX.as_bytes(), s.as_bytes());
80        let pss = Read::chain(ps, CONTAINER_SUFFIX.as_bytes());
81        quick_xml::de::from_reader(pss).map_err(io::Error::other)
82    }
83
84    pub fn into_pages(self) -> Vec<Page> {
85        self.pages
86    }
87}
88
89pub fn xmlpages2pages(xpages: &str) -> Result<Vec<Page>, io::Error> {
90    Container::from_pages_str(xpages).map(|c| c.into_pages())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use std::io;
97
98    /// A tiny helper that builds a `Container` from a string that already contains
99    /// one or more `<page>…</page>` fragments.
100    fn make_container(input: &str) -> io::Result<Container> {
101        Container::from_pages_str(input)
102    }
103
104    #[test]
105    fn test_container_parsing_single_page() {
106        // The XML string you hand‑in **does not** include the outer <container> tags –
107        // the helper will add them for you.
108        let xml = r#"
109            <page>
110                <title>Test Page</title>
111                <id>42</id>
112                <restrictions>none</restrictions>
113                <revision>
114                    <id>1000</id>
115                    <timestamp>2025-08-10T12:34:56Z</timestamp>
116                    <minor/>
117                    <comment>Initial revision</comment>
118                    <text>Page content goes here.</text>
119                </revision>
120            </page>
121        "#;
122
123        let container = make_container(xml).expect("Failed to parse container");
124        assert_eq!(container.pages.len(), 1);
125
126        let page = &container.pages[0];
127        assert_eq!(page.title.as_deref(), Some("Test Page"));
128        assert_eq!(page.id.as_deref(), Some("42"));
129        assert_eq!(page.restrictions.as_deref(), Some("none"));
130
131        let rev = page.revision.as_ref().expect("revision missing");
132        assert_eq!(rev.id.as_deref(), Some("1000"));
133        assert_eq!(rev.timestamp.as_deref(), Some("2025-08-10T12:34:56Z"));
134        assert_eq!(rev.comment.as_deref(), Some("Initial revision"));
135        assert_eq!(rev.text.as_deref(), Some("Page content goes here."));
136    }
137
138    #[test]
139    fn test_container_parsing_multiple_pages() {
140        // Two page fragments – the helper will wrap them with <container>.
141        let xml = r#"
142            <page>
143                <title>First</title>
144                <id>1</id>
145                <revision><id>10</id><timestamp>2025-01-01T00:00:00Z</timestamp></revision>
146            </page>
147            <page>
148                <title>Second</title>
149                <id>2</id>
150                <revision><id>20</id><timestamp>2025-02-01T00:00:00Z</timestamp></revision>
151            </page>
152        "#;
153
154        let container = make_container(xml).expect("Failed to parse container");
155        assert_eq!(container.pages.len(), 2);
156
157        // Check the first page
158        let p1 = &container.pages[0];
159        assert_eq!(p1.title.as_deref(), Some("First"));
160        assert_eq!(p1.id.as_deref(), Some("1"));
161        let r1 = p1.revision.as_ref().expect("revision missing");
162        assert_eq!(r1.id.as_deref(), Some("10"));
163        assert_eq!(r1.timestamp.as_deref(), Some("2025-01-01T00:00:00Z"));
164
165        // Check the second page
166        let p2 = &container.pages[1];
167        assert_eq!(p2.title.as_deref(), Some("Second"));
168        assert_eq!(p2.id.as_deref(), Some("2"));
169        let r2 = p2.revision.as_ref().expect("revision missing");
170        assert_eq!(r2.id.as_deref(), Some("20"));
171        assert_eq!(r2.timestamp.as_deref(), Some("2025-02-01T00:00:00Z"));
172    }
173
174    #[test]
175    fn test_container_parsing_error() {
176        // Malformed XML – missing the closing '>' on the second element.
177        let bad_xml = r#"<page><title>Oops</title></page><invalid"#;
178        assert!(make_container(bad_xml).is_err());
179    }
180}