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/>.
 */
//! This crate provides the proc_macro for [`mwapi_responses`](https://docs.rs/mwapi_responses/),
//! please refer to its documentation for usage.
#![deny(clippy::all)]
#![deny(rustdoc::all)]

mod builder;
mod metadata;
mod params;

extern crate proc_macro;
#[macro_use]
extern crate syn;

use crate::builder::{StructBuilder, StructField};
use crate::params::ParsedParams;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use std::collections::HashMap;
use syn::{DeriveInput, Ident, LitStr, Visibility};

type Result<T> = std::result::Result<T, syn::Error>;

#[proc_macro_attribute]
pub fn query(args: TokenStream, input: TokenStream) -> TokenStream {
    let mut params = HashMap::new();
    let arg_parser = syn::meta::parser(|meta| {
        let name = match meta.path.get_ident() {
            Some(ident) => ident.to_string(),
            None => return Err(meta.error("invalid parameter name")),
        };
        let value: LitStr = meta.value()?.parse()?;
        params.insert(name, value);
        Ok(())
    });
    parse_macro_input!(args with arg_parser);
    let input = parse_macro_input!(input as DeriveInput);

    let tokens = query_real(params, input)
        .unwrap_or_else(|err| err.into_compile_error());
    tokens.into()
}

fn query_real(
    params: HashMap<String, LitStr>,
    input: DeriveInput,
) -> Result<TokenStream2> {
    let params = ParsedParams::new(params)?;

    // Build the implementation
    impl_query(input, &params)
}

fn build_modules(
    prefix: &Ident,
    visibility: &Visibility,
    body_ident: &Ident,
    qparams: &ParsedParams,
) -> Result<(Vec<StructBuilder>, String, Ident)> {
    // dbg!(modules);
    let mut structs = vec![];
    let fieldname = qparams.get_fieldname()?;
    let container = qparams.get_fields()?;
    let mut top_fields: Vec<StructField> = vec![];
    for top_field in container.top.into_values() {
        top_fields.push(top_field.try_into()?);
    }
    for (subname, fields) in container.sub {
        let ident = format_ident!("{}Item{}", prefix, subname);
        let mut struct_fields: Vec<StructField> = vec![];
        for field in fields.into_values() {
            struct_fields.push(field.try_into()?);
        }
        // dbg!(&struct_fields);
        structs.push(StructBuilder {
            ident: ident.clone(),
            fields: struct_fields,
            visibility: visibility.clone(),
            extra_derive: None,
        });
        top_fields.push(StructField {
            name: subname,
            type_: quote! { Vec<#ident> },
            default: true,
            rename: None,
            deserialize_with: None,
        });
    }

    let item_ident = format_ident!("{}{}", prefix, "Item");
    // dbg!(&container);
    structs.push(StructBuilder {
        ident: item_ident.clone(),
        fields: top_fields,
        visibility: visibility.clone(),
        extra_derive: None,
    });
    let body_fields = vec![
        StructField {
            name: fieldname.to_string(),
            type_: quote! { Vec<#item_ident> },
            default: false,
            rename: None,
            deserialize_with: None,
        },
        StructField {
            name: "normalized".to_string(),
            type_: quote! { Vec<::mwapi_responses::normalize::Normalized> },
            default: true,
            rename: None,
            deserialize_with: None,
        },
        StructField {
            name: "redirects".to_string(),
            type_: quote! { Vec<::mwapi_responses::normalize::Redirect> },
            default: true,
            rename: None,
            deserialize_with: None,
        },
    ];
    structs.push(StructBuilder {
        ident: body_ident.clone(),
        fields: body_fields,
        visibility: visibility.clone(),
        extra_derive: Some(quote! { #[derive(Default)] }),
    });

    Ok((structs, fieldname, item_ident))
}

fn impl_query(
    input: DeriveInput,
    qparams: &ParsedParams,
) -> Result<TokenStream2> {
    let prefix = &input.ident;
    let visibility = &input.vis;
    let body_ident = format_ident!("{}Body", prefix);
    let (mut structs, item_fieldname, item_ident) =
        build_modules(prefix, visibility, &body_ident, qparams)?;
    let item_fieldname = format_ident!("{}", item_fieldname);
    // Add the main struct
    structs.push(StructBuilder {
        ident: prefix.clone(),
        fields: vec![
            StructField {
                name: "batchcomplete".to_string(),
                type_: quote! { bool },
                default: true,
                rename: None,
                deserialize_with: None,
            },
            StructField {
                name: "continue".to_string(),
                type_: quote! { ::std::collections::HashMap<String, String> },
                default: true,
                rename: Some("continue_".to_string()),
                deserialize_with: Some(
                    "::mwapi_responses::query::deserialize_continue"
                        .to_string(),
                ),
            },
            StructField {
                name: "query".to_string(),
                type_: quote! { #body_ident },
                default: true,
                rename: None,
                deserialize_with: None,
            },
        ],
        visibility: visibility.clone(),
        extra_derive: None,
    });

    let tokens = quote! {
        impl ::mwapi_responses::ApiResponse<#item_ident> for #prefix {
            fn params() -> &'static [(&'static str, &'static str)] {
                #qparams
            }

            fn items(&self) -> ::std::slice::Iter<'_, #item_ident> {
                self.query.#item_fieldname.iter()
            }

            fn into_items(self) -> ::std::vec::IntoIter<#item_ident> {
                self.query.#item_fieldname.into_iter()
            }

            fn redirects(&self) -> &[::mwapi_responses::normalize::Redirect] {
                &self.query.redirects
            }

            fn normalized_titles(&self) -> &[::mwapi_responses::normalize::Normalized] {
                &self.query.normalized
            }

        }

        #(#structs)*
    };
    Ok(tokens)
}