1use crate::{Site, Tool, ToolsError, fancy_title::FancyTitle};
19use async_trait::async_trait;
20use serde_json::{Value, json};
21
22#[derive(Debug, Default, PartialEq)]
23pub struct MissingTopics {
24 site: Site,
25 category_depth: Option<u32>,
26 category: Option<String>,
27 article: Option<String>,
28 occurs_more_often_than: Option<u32>,
29 no_template_links: Option<bool>,
30 no_singles: bool,
31
32 url_used: String,
33 results: Vec<(String, u64)>,
34 tool_url: String,
35}
36
37impl MissingTopics {
38 pub fn new(site: Site) -> Self {
40 Self {
41 site,
42 tool_url: "https://missingtopics.toolforge.org/".to_string(),
43 ..Default::default()
44 }
45 }
46
47 pub fn with_category(mut self, category: &str, category_depth: u32) -> Self {
51 self.category = Some(category.into());
52 self.category_depth = Some(category_depth);
53 self
54 }
55
56 pub fn with_article(mut self, article: &str) -> Self {
59 self.article = Some(article.into());
60 self
61 }
62
63 pub fn limit(mut self, occurs_more_often_than: u32) -> Self {
65 self.no_singles = true;
66 self.occurs_more_often_than = Some(occurs_more_often_than);
67 self
68 }
69
70 pub fn no_template_links(mut self, no_template_links: bool) -> Self {
72 self.no_template_links = Some(no_template_links);
73 self
74 }
75
76 pub fn url_used(&self) -> &str {
78 &self.url_used
79 }
80
81 pub fn results(&self) -> &[(String, u64)] {
84 &self.results
85 }
86
87 pub fn site(&self) -> &Site {
89 &self.site
90 }
91
92 pub async fn as_json(&self) -> Value {
93 let site = self.site();
94 let api = site.api().await.unwrap();
95 json!({
96 "pages": self.results()
97 .iter()
98 .map(|(prefixed_title,counter)| (FancyTitle::from_prefixed(prefixed_title, &api).to_json(),counter))
99 .map(|(mut v,counter)| {v["counter"] = json!(*counter); v})
100 .collect::<Vec<Value>>(),
101 "site": site,
102 })
103 }
104}
105
106#[async_trait]
107impl Tool for MissingTopics {
108 fn generate_paramters(&self) -> Result<Vec<(String, String)>, ToolsError> {
109 let mut parameters: Vec<(String, String)> = [
110 ("language".to_string(), self.site.language().to_string()),
111 ("project".to_string(), self.site.project().to_string()),
112 ("doit".to_string(), "Run".to_string()),
113 ("wikimode".to_string(), "json".to_string()),
114 ]
115 .to_vec();
116
117 if self.category.is_some() && self.category_depth.is_some() && self.article.is_some() {
118 return Err(ToolsError::Tool(
119 "Only one of category or article can be set".to_string(),
120 ));
121 }
122 if let (Some(category), Some(category_depth)) = (&self.category, &self.category_depth) {
123 parameters.push(("category".to_string(), category.to_string()));
124 parameters.push(("depth".to_string(), category_depth.to_string()));
125 } else if let Some(article) = &self.article {
126 parameters.push(("article".to_string(), article.to_string()));
127 } else {
128 return Err(ToolsError::Tool(
129 "Either category or article must be set".to_string(),
130 ));
131 }
132 match self.no_singles {
133 true => parameters.push(("nosingles".to_string(), "1".to_string())),
134 false => parameters.push(("nosingles".to_string(), "0".to_string())),
135 }
136 match self.no_template_links {
137 Some(true) => parameters.push(("no_template_links".to_string(), "1".to_string())),
138 Some(false) => parameters.push(("no_template_links".to_string(), "0".to_string())),
139 _ => {}
140 }
141 if let Some(occurs_more_often_than) = self.occurs_more_often_than {
142 parameters.push(("limitnum".to_string(), occurs_more_often_than.to_string()));
143 }
144 Ok(parameters)
145 }
146
147 #[cfg(feature = "tokio")]
148 async fn run(&mut self) -> Result<(), ToolsError> {
150 let url = &self.tool_url;
151 let parameters = self.generate_paramters()?;
152 let client = crate::ToolsInterface::tokio_client()?;
153 let response = client.get(url).query(¶meters).send().await?;
154 let j: Value = response.json().await?;
155 self.set_from_json(j)
156 }
157
158 #[cfg(feature = "blocking")]
159 fn run_blocking(&mut self) -> Result<(), ToolsError> {
161 let url = &self.tool_url;
162 let parameters = self.generate_paramters()?;
163 let client = crate::ToolsInterface::blocking_client()?;
164 let j: Value = client.get(url).query(¶meters).send()?.json()?;
165 self.set_from_json(j)
166 }
167
168 fn set_from_json(&mut self, j: Value) -> Result<(), ToolsError> {
169 if j["status"].as_str() != Some("OK") {
170 return Err(ToolsError::Tool(format!(
171 "MissingTopics status is not OK: {:?}",
172 j["status"]
173 )));
174 }
175 self.results = j["results"]
176 .as_object()
177 .ok_or(ToolsError::Json("['results'] has no object".into()))?
178 .iter()
179 .filter_map(|(k, v)| Some((k.to_string(), v.as_u64()?)))
180 .collect();
181 self.url_used = j["url"]
182 .as_str()
183 .ok_or(ToolsError::Json("['url'] is missing".into()))?
184 .to_string();
185 Ok(())
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::Site;
193 use serde_json::json;
194 use wiremock::matchers::{method, path, query_param_contains};
195 use wiremock::{Mock, MockServer, ResponseTemplate};
196
197 async fn get_mock_server() -> MockServer {
198 let mock_path = "/";
199 let mock_server = MockServer::start().await;
200 Mock::given(method("GET"))
201 .and(query_param_contains("language","de"))
202 .and(query_param_contains("project","wikipedia"))
203 .and(query_param_contains("article","Biologie"))
204 .and(query_param_contains("language","de"))
205 .and(query_param_contains("doit","Run"))
206 .and(query_param_contains("wikimode","json"))
207 .and(query_param_contains("no_template_links","1"))
208 .and(path(mock_path))
209 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"results":{"Ethnobiologie":4,"Landschaftsdiversität":3,"Micrographia":4,"Spezielle_Botanik":6,"Wbetavirus":4,"Zellphysiologie":4},"status":"OK","url":"https://missingtopics.toolforge.org/?language=de&project=wikipedia&depth=1&category=&article=Biologie&wikimode=json&limitnum=1¬emplatelinks=0"})))
210 .mount(&mock_server)
211 .await;
212 mock_server
213 }
214
215 #[cfg(feature = "tokio")]
216 #[tokio::test]
217 async fn test_missing_topics_run_async() {
218 let mock_server = get_mock_server().await;
219 let mut mt = MissingTopics::new(Site::from_wiki("dewiki").unwrap())
220 .with_article("Biologie")
221 .no_template_links(true);
222 mt.tool_url = format!("{}/", mock_server.uri());
223 mt.run().await.unwrap();
224 assert_eq!(mt.results.len(), 6);
225 assert_eq!(mt.results[5].0, "Zellphysiologie");
226 assert_eq!(mt.results[5].1, 4);
227 assert_eq!(
228 mt.url_used,
229 "https://missingtopics.toolforge.org/?language=de&project=wikipedia&depth=1&category=&article=Biologie&wikimode=json&limitnum=1¬emplatelinks=0"
230 )
231 }
232}