mwapi_responses_derive 0.5.1

Automatically generate strict types for MediaWiki API responses (macro)
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/>.
 */

use crate::metadata::{default_query_fields, FieldContainer, Metadata};
use crate::Result;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use std::collections::HashMap;
use syn::{Error, LitStr};

pub(crate) enum ParsedParams {
    List(LitStr, HashMap<String, String>),
    Props(Vec<LitStr>, HashMap<String, String>),
    None(HashMap<String, String>),
}

impl ParsedParams {
    pub fn new(mut input: HashMap<String, LitStr>) -> Result<Self> {
        let mut params = HashMap::new();
        if let Some(val) = input.remove("action") {
            if val.value() != "query" {
                return Err(Error::new(
                    val.span(),
                    "only action=query is supported",
                ));
            }
        }

        // TODO: meta= modules?
        let list = input.remove("list");
        let prop = input.remove("prop");

        for (key, value) in input.into_iter() {
            params.insert(key, value.value());
        }

        set_defaults(&mut params);

        let res = match (list, prop) {
            (None, None) => ParsedParams::None(params),
            (Some(list), Some(_)) => {
                return Err(Error::new(
                    list.span(),
                    "only one of list= or prop= can be specified, not both",
                ));
            }
            (Some(listname), None) => {
                params.insert("list".to_string(), listname.value());
                ParsedParams::List(listname, params)
            }
            (None, Some(prop)) => {
                params.insert("prop".to_string(), prop.value());
                let props: Vec<_> = prop
                    .value()
                    .split('|')
                    .map(|p| LitStr::new(p, prop.span()))
                    .collect();
                ParsedParams::Props(props, params)
            }
        };
        Ok(res)
    }

    pub(crate) fn get_fieldname(&self) -> Result<String> {
        let res = match self {
            ParsedParams::List(listname, _) => {
                Metadata::new(listname)?.fieldname
            }
            ParsedParams::Props(_, _) | ParsedParams::None(_) => {
                "pages".to_string()
            }
        };
        Ok(res)
    }

    fn get_params(&self) -> &HashMap<String, String> {
        match self {
            ParsedParams::List(_, params) => params,
            ParsedParams::Props(_, params) => params,
            ParsedParams::None(params) => params,
        }
    }

    fn get_fields_for(
        &self,
        module: &LitStr,
        container: &mut FieldContainer,
    ) -> Result<()> {
        let info = Metadata::new(module)?;
        let props: Vec<&str> = match &info.prop {
            Some(prop) => {
                match self.get_params().get(prop) {
                    Some(value) => value.split('|').collect(),
                    None => {
                        // FIXME: implement defaults
                        vec![]
                    }
                }
            }
            None => vec![],
        };
        let wrap_field = if info.wrap_in_vec {
            Some(info.fieldname.to_string())
        } else {
            None
        };
        container.add_fields(wrap_field, info.get_fields(&props));
        Ok(())
    }

    pub(crate) fn get_fields(&self) -> Result<FieldContainer> {
        let mut container = FieldContainer::default();
        match self {
            ParsedParams::List(listname, _) => {
                self.get_fields_for(listname, &mut container)?;
            }
            ParsedParams::Props(props, _) => {
                // These are always top-level fields, so add them directly
                container.top.extend(default_query_fields());
                for prop in props {
                    self.get_fields_for(prop, &mut container)?;
                }
            }
            ParsedParams::None(_) => {
                container.top.extend(default_query_fields());
            }
        }
        Ok(container)
    }
}

/// Turn the HashMap of params into a slice that returns
/// `&[(&str, &str)]`, which is the format used by mwapi and trivially
/// convertable into a HashMap<String, String> for mediawiki-rs.
impl ToTokens for ParsedParams {
    fn to_tokens(&self, tokens: &mut TokenStream2) {
        let params = self.get_params();
        let keys = params.keys();
        let values = params.values();

        let stream = quote! {
            &[
                #((#keys, #values),)*
            ]
        };
        stream.to_tokens(tokens);
    }
}

fn set_defaults(params: &mut HashMap<String, String>) {
    // Force action=query
    params.insert("action".to_string(), "query".to_string());
    // Force format=json&formatversion=2
    params.insert("format".to_string(), "json".to_string());
    params.insert("formatversion".to_string(), "2".to_string());
}