mwapi_responses 0.5.1

Automatically generate strict types for MediaWiki API responses
Documentation
/*
Copyright (C) 2020-2021 Kunal Mehta <legoktm@debian.org>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
//! The `mwapi_responses` crate provides strict typing for dynamic MediaWiki
//! Action API queries. The goal is to faithfully represent what the API's
//! JSON structure is while saving people from manually writing out
//! serde-compatible structs for every query.
//!
//! A list module:
//! ```
//! use mwapi_responses::prelude::*;
//! #[query(
//!     list = "logevents",
//!     leaction = "newusers/create",
//!     leprop = "user|type",
//!     lelimit = "100"
//! )]
//! struct Response;
//! ```
//!
//! This creates a `Response` struct that matches the API response for the given
//! query. Roughly, it will expand to:
//!
//! ```ignore
//! #[derive(Debug, Clone, serde::Deserialize)]
//! pub struct Response {
//!     #[serde(default)]
//!     pub batchcomplete: bool,
//!     #[serde(rename = "continue")]
//!     #[serde(default)]
//!     pub continue_: HashMap<String, String>,
//!     pub query: ResponseBody,
//! }
//!
//! #[derive(Debug, Clone, serde::Deserialize)]
//! pub struct ResponseBody {
//!     pub logevents: Vec<ResponseItemlogevents>,
//! }
//!
//! #[derive(Debug, Clone, serde::Deserialize)]
//! pub struct ResponseItemlogevents {
//!     pub user: String,
//!     #[serde(rename = "type")]
//!     pub type_: String,
//! }
//! ```
//!
//! Or getting various properties from pages:
//! ```
//! use mwapi_responses::prelude::*;
//! #[query(
//!     prop="info|revisions",
//!     inprop="url",
//!     rvprop="ids"
//! )]
//! struct Response;
//! ```
//! which expands to:
//! ```ignore
//! #[derive(Debug, Clone, serde::Deserialize)]
//! pub struct Response {
//!     #[serde(default)]
//!     pub batchcomplete: bool,
//!     #[serde(rename = "continue")]
//!     #[serde(default)]
//!     pub continue_: HashMap<String, String>,
//!     pub query: ResponseBody,
//! }
//!
//! #[derive(Debug, Clone, serde::Deserialize)]
//! pub struct ResponseBody {
//!     pub pages: Vec<ResponseItem>,
//! }
//!
//! #[derive(Debug, Clone, serde::Deserialize)]
//! pub struct ResponseItem {
//!     pub canonicalurl: String,
//!     pub contentmodel: String,
//!     pub editurl: String,
//!     pub fullurl: String,
//!     pub lastrevid: Option<u64>,
//!     pub length: Option<u32>,
//!     #[serde(default)]
//!     pub missing: bool,
//!     #[serde(default)]
//!     pub new: bool,
//!     pub ns: i32,
//!     pub pageid: Option<u32>,
//!     pub pagelanguage: String,
//!     pub pagelanguagedir: String,
//!     pub pagelanguagehtmlcode: String,
//!     #[serde(default)]
//!     pub redirect: bool,
//!     pub title: String,
//!     pub touched: Option<String>,
//!     #[serde(default)]
//!     pub revisions: Vec<ResponseItemrevisions>,
//! }
//!
//! #[derive(Debug, Clone, serde::Deserialize)]
//! pub struct ResponseItemrevisions {
//!     pub parentid: u64,
//!     pub revid: u64,
//! }
//! ```
//!
//! In both cases, you can easily iterate over `ResponseItemlogevents`
//! and `ResponseItem` respectively by calling `Response::items()`.
//!
//! To avoid repeating the parameters in multiple places, you can call
//! `Response::params()` to get a `&[(&str, &str)]` of the parameters that
//! were provided to the `#[query(...)]` macro.
//!
//! Fields are renamed if they can't be used as fields in Rust, like `continue`
//! or `type`. In these cases, an underscore is appended.
//!
//! ## Supported modules
//! The metadata that powers this crate is manually gathered. To see if
//! a specific module is supported, look at the [Git repository](https://gitlab.wikimedia.org/repos/mwbot-rs/mwbot/-/tree/main/mwapi_responses_derive/data).
//! Contributions for missing modules or parameters are always welcome.
//!
//! ## Library agnostic
//! This crate does not implement or support any one specific API or HTTP
//! library, rather it aims to just provide types and helpers to enable you to
//! run and execute your API requests however you'd like.
//!
//! The [`mwapi`](https://docs.rs/mwapi) crate provides a convenience function
//! to execute a query using these generated responses: `query_response()`.
//!
//! ## Future plans
//! There is no special support for continuing queries. In the future some
//! merge()-like function might be provided.
//!
//! Some other ideas are outlined in [a blog post by Legoktm](https://blog.legoktm.com/2021/11/01/generating-rust-types-for-mediawiki-api-responses.html).
//!
//! ## Contributing
//! `mwapi_responses` is a part of the [`mwbot-rs` project](https://www.mediawiki.org/wiki/Mwbot-rs).
//! We're always looking for new contributors, please [reach out](https://www.mediawiki.org/wiki/Mwbot-rs#Contributing)
//! if you're interested!
#![deny(clippy::all)]
#![deny(rustdoc::all)]

pub mod block;
pub mod categoryinfo;
pub mod lint;
pub mod misc;
pub mod normalize;
pub mod protection;
pub mod query;
pub mod timestamp;

#[cfg(feature = "derive")]
pub use mwapi_responses_derive::query;
/// mwtimestamp is re-exported for use in the macro
#[doc(hidden)]
pub use mwtimestamp;
/// serde is re-exported for use in the macro
#[doc(hidden)]
pub use serde;
use std::collections::HashMap;
pub mod prelude {
    #[cfg(feature = "derive")]
    pub use crate::query;
    pub use crate::ApiResponse;
}
use std::slice::Iter;
use std::vec::IntoIter;

pub trait ApiResponse<T> {
    /// Get the request params to send to the API
    fn params() -> &'static [(&'static str, &'static str)];

    /// Iterate over the main items in this request
    fn items(&self) -> Iter<'_, T>;

    /// Iterate and own over main items in this request
    fn into_items(self) -> IntoIter<T>;

    /// Private, for internal usage only
    #[doc(hidden)]
    fn normalized_titles(&self) -> &[normalize::Normalized];

    /// Private, for internal usage only
    #[doc(hidden)]
    fn redirects(&self) -> &[normalize::Redirect];

    /// Get a map of normalized/redirects all merged
    fn title_map(&self) -> HashMap<String, String> {
        let normalized: HashMap<String, String> = self
            .normalized_titles()
            .iter()
            .map(|norm| (norm.from.to_string(), norm.to.to_string()))
            .collect();
        let mut redirects: HashMap<String, String> = self
            .redirects()
            .iter()
            .map(|redir| (redir.from.to_string(), redir.to.to_string()))
            .collect();
        let mut titles = HashMap::new();
        for (from, to) in normalized {
            // See if the normalized title is also a redirect
            let real_to = redirects.remove(&to).unwrap_or(to);
            titles.insert(from, real_to);
        }
        // Merge in the remaining redirects
        titles.extend(redirects);

        titles
    }
}

/// Easily execute a request using the `mwapi` crate.
///
/// Any extra custom parameters can be passed in to the function and were
/// merged with the default request parameters.
#[cfg(feature = "mwapi-07")]
pub async fn query_api<
    T: ApiResponse<U> + serde::de::DeserializeOwned,
    U,
    P: Into<mwapi::Params>,
>(
    client: &mwapi::Client,
    extra: P,
) -> mwapi::Result<T> {
    let extra = extra.into();
    let mut params = T::params().to_vec();
    for (name, value) in extra.as_map() {
        params.push((name, value));
    }
    client.get(&params).await
}