mwapi_responses_derive 0.5.1

Automatically generate strict types for MediaWiki API responses (macro)
Documentation
/*
Copyright (C) 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 proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, ToTokens};
use syn::{Ident, Visibility};

pub(crate) struct StructBuilder {
    pub(crate) ident: Ident,
    pub(crate) fields: Vec<StructField>,
    pub(crate) visibility: Visibility,
    pub(crate) extra_derive: Option<TokenStream2>,
}

impl ToTokens for StructBuilder {
    fn to_tokens(&self, tokens: &mut TokenStream2) {
        let ident = &self.ident;
        let visibility = &self.visibility;
        let extra_derive = &self.extra_derive;
        let fields = &self.fields;
        let stream = quote! {
            #[doc(hidden)]
            #extra_derive
            #[derive(Debug, Clone, ::mwapi_responses::serde::Deserialize)]
            #[serde(crate = "::mwapi_responses::serde")]
            #visibility struct #ident {
                #(#fields)*
            }
        };

        stream.to_tokens(tokens);
    }
}

#[derive(Debug)]
pub(crate) struct StructField {
    pub(crate) name: String,
    pub(crate) type_: TokenStream2,
    pub(crate) default: bool,
    pub(crate) rename: Option<String>,
    pub(crate) deserialize_with: Option<String>,
}

impl ToTokens for StructField {
    /// A field line looks like:
    /// ```ignore
    /// #[serde(default)] #[serde(rename = "foo")] foo: String;
    /// ```
    /// The serde attributes are optional.
    ///
    fn to_tokens(&self, tokens: &mut TokenStream2) {
        let (name, rename) = match &self.rename {
            Some(rename) => {
                let name = &self.name;
                (
                    rename.to_string(),
                    Some(quote! {
                        #[serde(rename = #name)]
                    }),
                )
            }
            None => (self.name.to_string(), None),
        };
        let name = format_ident!("{}", name);
        let type_ = &self.type_;
        let default = if self.default {
            Some(quote! {
                #[serde(default)]
            })
        } else {
            None
        };
        let deser_with = self.deserialize_with.as_ref().map(|function| {
            quote! {
                #[serde(deserialize_with = #function)]
            }
        });
        let stream = quote! {
            #default #rename #deser_with pub #name: #type_,
        };

        stream.to_tokens(tokens);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    fn stringify<P: ToTokens>(thing: P) -> String {
        let mut stream = quote! {};
        thing.to_tokens(&mut stream);
        stream.to_string()
    }

    #[test]
    fn test_builder() {
        let builder = StructBuilder {
            ident: format_ident!("{}", "Name"),
            fields: vec![StructField {
                name: "abc".to_string(),
                type_: quote! { u32 },
                default: false,
                rename: None,
                deserialize_with: None,
            }],
            visibility: Visibility::Inherited,
            extra_derive: None,
        };
        assert_eq!(
            &stringify(builder),
            "# [doc (hidden)] # [derive (Debug , Clone , :: mwapi_responses :: serde :: Deserialize)] # [serde (crate = \"::mwapi_responses::serde\")] struct Name { pub abc : u32 , }"
        );
    }

    #[test]
    fn test_field() {
        let field = StructField {
            name: "abc".to_string(),
            type_: quote! { u32 },
            default: false,
            rename: None,
            deserialize_with: None,
        };
        assert_eq!(&stringify(field), "pub abc : u32 ,");
        let field = StructField {
            name: "abc".to_string(),
            type_: quote! { u32 },
            default: true,
            rename: None,
            deserialize_with: None,
        };
        assert_eq!(&stringify(field), "# [serde (default)] pub abc : u32 ,");
        let field = StructField {
            name: "abc".to_string(),
            type_: quote! { u32 },
            default: true,
            rename: Some("def".to_string()),
            deserialize_with: None,
        };
        assert_eq!(
            &stringify(field),
            "# [serde (default)] # [serde (rename = \"abc\")] pub def : u32 ,"
        );
        let field = StructField {
            name: "abc".to_string(),
            type_: quote! { u32 },
            default: false,
            rename: None,
            deserialize_with: Some("::foo::bar".to_string()),
        };
        assert_eq!(
            &stringify(field),
            "# [serde (deserialize_with = \"::foo::bar\")] pub abc : u32 ,"
        );
    }
}