dioxus-js-bindgen-macro 0.1.1

Procedural macro engine for dioxus-js-bindgen FFI bridge
Documentation
use syn::parse::{Parse, ParseStream};
use syn::{Attribute, Ident, LitStr, Result, Token};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PillarAttr {
    Command,
    Query,
    Watcher,
}

#[derive(Debug, Clone)]
pub struct ItemSpec {
    pub attr: Option<PillarAttr>,
    pub original_name: String,
    pub rename_as: Option<Ident>,
}

#[derive(Debug, Clone)]
pub struct BindJsInput {
    pub file_path: LitStr,
    pub wildcard: bool,
    pub items: Vec<ItemSpec>,
}

impl Parse for BindJsInput {
    fn parse(input: ParseStream) -> Result<Self> {
        let file_path: LitStr = input.parse()?;
        input.parse::<Token![::]>()?;

        if input.peek(Token![*]) {
            input.parse::<Token![*]>()?;
            let _ = input.parse::<Option<Token![;]>>()?;
            return Ok(Self {
                file_path,
                wildcard: true,
                items: Vec::new(),
            });
        }

        let content;
        syn::braced!(content in input);

        let mut wildcard = false;
        let mut items = Vec::new();

        while !content.is_empty() {
            if content.peek(Token![*]) {
                content.parse::<Token![*]>()?;
                wildcard = true;
                let _ = content.parse::<Option<Token![,]>>()?;
                continue;
            }

            let mut attr = None;
            if content.peek(Token![#]) {
                let parsed_attrs = content.call(Attribute::parse_outer)?;
                for a in parsed_attrs {
                    if a.path().is_ident("command") {
                        attr = Some(PillarAttr::Command);
                    } else if a.path().is_ident("query") {
                        attr = Some(PillarAttr::Query);
                    } else if a.path().is_ident("watcher") || a.path().is_ident("monitor") {
                        attr = Some(PillarAttr::Watcher);
                    } else {
                        return Err(syn::Error::new_spanned(
                            a,
                            "Unknown attribute for bind_js item. Allowed: #[command], #[query], #[watcher]",
                        ));
                    }
                }
            }

            let _ = content.parse::<Option<Token![fn]>>()?;
            let name_ident: Ident = content.parse()?;
            let original_name = name_ident.to_string();

            let mut rename_as = None;
            if content.peek(Token![as]) {
                content.parse::<Token![as]>()?;
                rename_as = Some(content.parse::<Ident>()?);
            }

            items.push(ItemSpec {
                attr,
                original_name,
                rename_as,
            });

            let _ = content.parse::<Option<Token![,]>>()?;
        }

        let _ = input.parse::<Option<Token![;]>>()?;

        Ok(Self {
            file_path,
            wildcard,
            items,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_wildcard() {
        let input: BindJsInput = syn::parse_str(r#""src/dom.ts"::*"#).unwrap();
        assert_eq!(input.file_path.value(), "src/dom.ts");
        assert!(input.wildcard);
        assert!(input.items.is_empty());
    }

    #[test]
    fn test_parse_block() {
        let input: BindJsInput = syn::parse_str(
            r#"
            "src/dom.ts":: {
                focusElement,
                #[command] playSound as play_sound,
                #[query] getRect,
                #[watcher] watchScroll as on_scroll,
                *
            }
            "#,
        )
        .unwrap();

        assert_eq!(input.file_path.value(), "src/dom.ts");
        assert!(input.wildcard);
        assert_eq!(input.items.len(), 4);

        assert_eq!(input.items[0].original_name, "focusElement");
        assert_eq!(input.items[0].attr, None);
        assert_eq!(input.items[0].rename_as, None);

        assert_eq!(input.items[1].original_name, "playSound");
        assert_eq!(input.items[1].attr, Some(PillarAttr::Command));
        assert_eq!(input.items[1].rename_as.as_ref().unwrap().to_string(), "play_sound");

        assert_eq!(input.items[2].original_name, "getRect");
        assert_eq!(input.items[2].attr, Some(PillarAttr::Query));

        assert_eq!(input.items[3].original_name, "watchScroll");
        assert_eq!(input.items[3].attr, Some(PillarAttr::Watcher));
        assert_eq!(input.items[3].rename_as.as_ref().unwrap().to_string(), "on_scroll");
    }
}