rs_wikipages2struct/
lib.rs1use 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 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 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 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 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 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 let bad_xml = r#"<page><title>Oops</title></page><invalid"#;
178 assert!(make_container(bad_xml).is_err());
179 }
180}