use std::collections::{BTreeMap, HashSet}; use reqwest::Url; use serde::Deserialize; use serde_json::Value; /// A Rosetta Code task. #[derive(Clone, PartialEq, Eq, Hash, Debug, Deserialize)] pub struct Task { /// The ID of the page containing the task in the MediaWiki API. #[serde(rename = "pageid")] pub id: u64, /// The human-readable title of the task. pub title: String, } /// Encapsulates errors that might occur during JSON parsing. #[derive(Debug)] enum TaskParseError { /// Something went wrong with the HTTP request to the API. Http(reqwest::Error), /// There was a problem parsing the API response into JSON. Json(serde_json::Error), /// The response JSON contained unexpected keys or values. UnexpectedFormat, } impl From for TaskParseError { fn from(err: serde_json::Error) -> Self { TaskParseError::Json(err) } } impl From for TaskParseError { fn from(err: reqwest::Error) -> Self { TaskParseError::Http(err) } } /// Represents a category of pages on Rosetta Code, such as "Rust". struct Category { name: String, continue_params: Option>, } impl Category { fn new(name: &str) -> Category { let mut continue_params = BTreeMap::new(); continue_params.insert("continue".to_owned(), "".to_owned()); Category { name: name.to_owned(), continue_params: Some(continue_params), } } } /// Sends a request to Rosetta Code through the MediaWiki API. If successful, returns the response /// as a JSON object. fn query_api( category_name: &str, continue_params: &BTreeMap, ) -> Result { let mut url = Url::parse("http://rosettacode.org/mw/api.php").expect("invalid URL"); url.query_pairs_mut() .append_pair("action", "query") .append_pair("list", "categorymembers") .append_pair("cmtitle", &format!("Category:{}", category_name)) .append_pair("cmlimit", "500") .append_pair("format", "json") .extend_pairs(continue_params); Ok(reqwest::blocking::get(url)?.json()?) } /// Given a JSON object, parses the task information from the MediaWiki API response. fn parse_tasks(json: &Value) -> Result, TaskParseError> { let tasks_json = json .pointer("/query/categorymembers") .and_then(Value::as_array) .ok_or(TaskParseError::UnexpectedFormat)?; tasks_json .iter() .map(|json| Task::deserialize(json).map_err(From::from)) .collect() } impl Iterator for Category { type Item = Vec; fn next(&mut self) -> Option { self.continue_params.as_ref()?; query_api(&self.name, self.continue_params.as_ref()?) .and_then(|result| { // If there are more pages of results to request, save them for the next iteration. self.continue_params = result .get("continue") .and_then(Value::as_object) .map(|continue_params| { continue_params .iter() .map(|(key, value)| { (key.to_owned(), value.as_str().unwrap().to_owned()) }) .collect() }); parse_tasks(&result) }) .map_err(|err| println!("Error parsing response: {:?}", err)) .ok() } } pub fn all_tasks() -> Vec { Category::new("Programming Tasks").flatten().collect() } pub fn unimplemented_tasks(lang: &str) -> Vec { let all_tasks = all_tasks().iter().cloned().collect::>(); let implemented_tasks = Category::new(lang).flatten().collect::>(); let mut unimplemented_tasks = all_tasks .difference(&implemented_tasks) .cloned() .collect::>(); unimplemented_tasks.sort_by(|a, b| a.title.cmp(&b.title)); unimplemented_tasks } fn main() { for task in find_unimplemented_tasks::unimplemented_tasks("Rust") { println!("{:6} {}", task.id, task.title); } }