#![deny(missing_docs)]
use serde_json::Value;
use crate::action_api::{ActionApiContinuable, ActionApiRunnable};
use crate::{Api, ApiSync, MediaWikiError};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PageContext {
pub pageid: Option<u64>,
pub ns: i64,
pub title: String,
}
impl PageContext {
pub fn from_value(page: &Value) -> Self {
Self {
pageid: page["pageid"].as_u64(),
ns: page["ns"].as_i64().unwrap_or(0),
title: page["title"].as_str().unwrap_or("").to_string(),
}
}
}
pub trait PageQueryResult: Sized {
fn from_page_value(page: &Value) -> Vec<Self>;
}
fn iter_pages(pages: &Value) -> Vec<&Value> {
if let Some(obj) = pages.as_object() {
obj.values().collect()
} else if let Some(arr) = pages.as_array() {
arr.iter().collect()
} else {
Vec::new()
}
}
#[derive(Debug, Clone)]
pub struct PageQueryResultList<T> {
items: Vec<T>,
}
impl<T> Default for PageQueryResultList<T> {
fn default() -> Self {
Self { items: Vec::new() }
}
}
impl<T: PageQueryResult> PageQueryResultList<T> {
pub fn new() -> Self {
Self::default()
}
pub fn from_result(result: &Value) -> Self {
let mut list = Self::new();
list.add_from_result(result);
list
}
pub fn add_from_result(&mut self, result: &Value) {
let pages = &result["query"]["pages"];
for page_value in iter_pages(pages) {
self.items.extend(T::from_page_value(page_value));
}
}
pub fn items(&self) -> &[T] {
&self.items
}
pub fn items_mut(&mut self) -> &mut Vec<T> {
&mut self.items
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub async fn fetch_all<B>(
builder: &B,
api: &Api,
max: Option<usize>,
) -> Result<Self, MediaWikiError>
where
B: ActionApiRunnable + ActionApiContinuable + Clone + Sync,
{
let mut list = Self::new();
let mut builder = builder.clone();
loop {
let result = builder.run(api).await?;
list.add_from_result(&result);
if let Some(max) = max {
if list.len() >= max {
list.items.truncate(max);
break;
}
}
if !builder.has_more(&result) {
break;
}
builder = builder.continue_from(&result);
}
Ok(list)
}
pub fn fetch_all_sync<B>(
builder: &B,
api: &ApiSync,
max: Option<usize>,
) -> Result<Self, MediaWikiError>
where
B: ActionApiRunnable + ActionApiContinuable + Clone + Sync,
{
let mut list = Self::new();
let mut builder = builder.clone();
loop {
let result = builder.run_sync(api)?;
list.add_from_result(&result);
if let Some(max) = max {
if list.len() >= max {
list.items.truncate(max);
break;
}
}
if !builder.has_more(&result) {
break;
}
builder = builder.continue_from(&result);
}
Ok(list)
}
}
impl<T> IntoIterator for PageQueryResultList<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.items.into_iter()
}
}
impl<'a, T> IntoIterator for &'a PageQueryResultList<T> {
type Item = &'a T;
type IntoIter = std::slice::Iter<'a, T>;
fn into_iter(self) -> Self::IntoIter {
self.items.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
struct TitleOnly(String);
impl PageQueryResult for TitleOnly {
fn from_page_value(page: &Value) -> Vec<Self> {
page["title"]
.as_str()
.map(|s| vec![TitleOnly(s.to_string())])
.unwrap_or_default()
}
}
struct LinkTitle(String);
impl PageQueryResult for LinkTitle {
fn from_page_value(page: &Value) -> Vec<Self> {
page["links"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v["title"].as_str().map(|s| LinkTitle(s.to_string())))
.collect()
})
.unwrap_or_default()
}
}
#[test]
fn page_context_from_value() {
let v = json!({"pageid": 42, "ns": 1, "title": "Talk:Test"});
let ctx = PageContext::from_value(&v);
assert_eq!(ctx.pageid, Some(42));
assert_eq!(ctx.ns, 1);
assert_eq!(ctx.title, "Talk:Test");
}
#[test]
fn page_context_missing_page() {
let v = json!({"ns": 0, "title": "Missing", "missing": ""});
let ctx = PageContext::from_value(&v);
assert_eq!(ctx.pageid, None);
}
#[test]
fn page_level_from_result_v1() {
let result = json!({
"query": {
"pages": {
"1": {"pageid": 1, "ns": 0, "title": "Alpha"},
"2": {"pageid": 2, "ns": 0, "title": "Beta"}
}
}
});
let list = PageQueryResultList::<TitleOnly>::from_result(&result);
assert_eq!(list.len(), 2);
}
#[test]
fn page_level_from_result_v2() {
let result = json!({
"query": {
"pages": [
{"pageid": 1, "ns": 0, "title": "Alpha"},
{"pageid": 2, "ns": 0, "title": "Beta"}
]
}
});
let list = PageQueryResultList::<TitleOnly>::from_result(&result);
assert_eq!(list.len(), 2);
assert_eq!(list.items()[0].0, "Alpha");
assert_eq!(list.items()[1].0, "Beta");
}
#[test]
fn sub_array_from_result() {
let result = json!({
"query": {
"pages": {
"1": {
"pageid": 1, "ns": 0, "title": "Test",
"links": [
{"ns": 0, "title": "Link1"},
{"ns": 0, "title": "Link2"}
]
}
}
}
});
let list = PageQueryResultList::<LinkTitle>::from_result(&result);
assert_eq!(list.len(), 2);
assert_eq!(list.items()[0].0, "Link1");
assert_eq!(list.items()[1].0, "Link2");
}
#[test]
fn add_from_result_accumulates() {
let r1 = json!({"query": {"pages": {"1": {"title": "A"}}}});
let r2 = json!({"query": {"pages": [{"title": "B"}, {"title": "C"}]}});
let mut list = PageQueryResultList::<TitleOnly>::new();
assert!(list.is_empty());
list.add_from_result(&r1);
assert_eq!(list.len(), 1);
list.add_from_result(&r2);
assert_eq!(list.len(), 3);
}
#[test]
fn empty_result() {
let list = PageQueryResultList::<TitleOnly>::from_result(&json!({}));
assert!(list.is_empty());
}
#[test]
fn into_iterator() {
let result = json!({"query": {"pages": [{"title": "A"}, {"title": "B"}]}});
let list = PageQueryResultList::<TitleOnly>::from_result(&result);
let titles: Vec<String> = list.into_iter().map(|t| t.0).collect();
assert_eq!(titles, vec!["A", "B"]);
}
#[test]
fn ref_iterator() {
let result = json!({"query": {"pages": [{"title": "A"}]}});
let list = PageQueryResultList::<TitleOnly>::from_result(&result);
let titles: Vec<&str> = (&list).into_iter().map(|t| t.0.as_str()).collect();
assert_eq!(titles, vec!["A"]);
assert_eq!(list.len(), 1); }
#[test]
fn items_mut_allows_modification() {
let result = json!({"query": {"pages": [{"title": "A"}, {"title": "B"}]}});
let mut list = PageQueryResultList::<TitleOnly>::from_result(&result);
list.items_mut().retain(|t| t.0 == "A");
assert_eq!(list.len(), 1);
}
}