papito_codegen 0.1.0

Codegen for the Papito WASM Framework
Documentation
use heck::SnakeCase;
use quote::Tokens;
use syn::{Attribute, Field, Fields, FieldsNamed, Ident, Item, ItemStruct, Visibility, Path};

pub fn quote(state: Item) -> Tokens {
    match state {
        Item::Struct(ref struct_item) => {
            quote_struct_item(struct_item)
        }
        Item::Fn(_) => {
            quote! {}
        }
        _ => {
            panic!("The attribute is only allowed for fns and structs");
        }
    }
}

fn quote_struct_item(item: &ItemStruct) -> Tokens {
    let state_ident = &item.ident;
    let props_ident = &Ident::from(format!("{}Prop", item.ident));
    let comp_ident = &Ident::from(format!("{}Component", item.ident));
    let state_fields = &item.fields;
    let vis = &item.vis;
    let props_struct = generate_props_struct(props_ident, state_fields);
    let augmented_state = augment_state_struct(item.attrs.clone(), vis, state_ident, state_fields);
    let assert_lifecycle = assert_lifecycle(state_ident);
    let comp_struct = generate_component_struct(vis, comp_ident, state_ident);
    let component_of = impl_component_of(comp_ident, state_ident);
    let component_impl = impl_component_trait(comp_ident, state_ident, props_ident, state_fields);
    let lifecycle_impl = impl_lifecycle_for_comp(comp_ident);
    let state_setters = impl_state_setters_and_notifier(state_ident, state_fields);
    quote! {
        #augmented_state

        #assert_lifecycle

        #comp_struct

        #component_of

        #component_impl

        #lifecycle_impl

        #state_setters

        #props_struct
    }
}

fn impl_component_of(comp: &Ident, state: &Ident) -> Tokens {
    quote! {
        impl ::papito_dom::ComponentOf for #state {
            type Comp = #comp;
        }
    }
}

fn generate_component_struct(vis: &Visibility, comp_ident: &Ident, state_ident: &Ident) -> Tokens {
    quote! {
        #vis struct #comp_ident {
            inner: ::std::rc::Rc<::std::cell::RefCell<#state_ident>>
        }
    }
}

fn assert_lifecycle(state: &Ident) -> Tokens {
    let mod_ = Ident::from(format!("{}StateAssertions", state).to_snake_case());
    quote! {
        mod #mod_ {
            struct _AssertLifecycle where #state: ::papito_dom::Lifecycle;
        }
    }
}

fn impl_lifecycle_for_comp(comp: &Ident) -> Tokens {
    quote! {
        impl ::papito::prelude::Lifecycle for #comp {
            fn created(&mut self) {
                self.inner.borrow_mut().created();
            }

            fn mounted(&mut self) {
                self.inner.borrow_mut().mounted();
            }

            fn updated(&mut self) {
                self.inner.borrow_mut().updated();
            }

            fn destroyed(&mut self) {
                self.inner.borrow_mut().destroyed();
            }
        }
    }
}

fn augment_state_struct(attrs: Vec<Attribute>, vis: &Visibility, state_ident: &Ident, fields: &Fields) -> Tokens {
    let notifier = Ident::from("notifier".to_string());
    match *fields {
        Fields::Named(ref fields_named) => {
            let named = sanitize_fields(fields_named);
            quote! {
                #(#attrs)*
                #vis struct #state_ident {
                    #(#named),*,
                    #notifier: Box<Fn()>
                }
            }
        }
        Fields::Unnamed(_) => {
            panic!("Tuple structs are not supported as components");
        }
        Fields::Unit => {
            quote! {
                #(#attrs)*
                #vis struct #state_ident;
            }
        }
    }
}

fn sanitize_fields(fields_named: &FieldsNamed) -> Vec<Field> {
    let prop_path = Path::from(Ident::from("prop".to_string()));
    fields_named.named.clone().into_iter()
        .map(|mut it| {
            it.attrs = it.attrs.into_iter().filter(|attr| attr.path != prop_path)
                .collect();
            it
        }).collect::<Vec<Field>>()
}

fn impl_component_trait(comp_ident: &Ident, state_ident: &Ident, props_ident: &Ident, fields: &Fields) -> Tokens {
    let create_fn = impl_create_fn(comp_ident, state_ident, fields);
    let update_fn = impl_update_fn(fields);
    let props_eq_fn = impl_eq_props_fn(fields);
    let props_ty = if get_props_from_fields(fields).is_empty() {
        quote! {
            ()
        }
    } else {
        quote! {
            #props_ident
        }
    };
    quote! {
        impl ::papito_dom::Component for #comp_ident {
            type Props = #props_ty;

            #create_fn

            #update_fn

            #props_eq_fn
        }
    }
}

fn impl_create_fn(comp_ident: &Ident, state_ident: &Ident, fields: &Fields) -> Tokens {
    match *fields {
        Fields::Named(ref fields_named) => {
            quote_fields_named(comp_ident, state_ident, fields_named)
        }
        Fields::Unnamed(_) => {
            panic!("Tuple structs are not supported as components");
        }
        Fields::Unit => {
            quote_unit_field(comp_ident, state_ident)
        }
    }
}

fn quote_fields_named(comp_ident: &Ident, state_ident: &Ident, fields: &FieldsNamed) -> Tokens {
    let mut field_inits = vec![];
    for field in fields.named.iter() {
        let ident = &field.ident.unwrap();
        if !is_prop_field(field) {
            field_inits.push(quote! {
                #ident: Default::default()
            });
        } else {
            field_inits.push(quote! {
                #ident: props.#ident
            });
        }
    }
    quote! {
        fn create(props: Self::Props, notifier: Box<Fn()>) -> Self {
            let state = #state_ident {
                #(#field_inits),*,
                notifier
            };
            #comp_ident {
                inner: ::std::rc::Rc::new(::std::cell::RefCell::new(state))
            }
        }
    }
}

fn quote_unit_field(comp_ident: &Ident, state_ident: &Ident) -> Tokens {
    quote! {
        fn create(_: Self::Props, _: Box<Fn()>) -> Self {
            let state = #state_ident;
            #comp_ident {
                inner: ::std::rc::Rc::new(::std::cell::RefCell::new(state))
            }
        }
    }
}

fn impl_update_fn(fields: &Fields) -> Tokens {
    let prop_fields = get_props_from_fields(fields);
    if !prop_fields.is_empty() {
        let props_tokens = prop_fields.iter().map(|it| {
            let ident = &it.ident;
            quote! {
                inner.#ident = props.#ident;
            }
        }).collect::<Vec<_>>();
        quote! {
            fn update(&mut self, props: Self::Props) {
                {
                    let inner = &mut *self.inner.borrow_mut();
                    #(#props_tokens)*
                }
                self.inner.borrow().notify();
            }
        }
    } else {
        quote! {
            fn update(&mut self, _: Self::Props) {}
        }
    }
}

fn impl_eq_props_fn(fields: &Fields) -> Tokens {
    let prop_fields = get_props_from_fields(fields);
    if !prop_fields.is_empty() {
        let comparisons = prop_fields.iter()
            .map(|it| {
                let ident = &it.ident;
                quote! {
                    inner.#ident == rhs.#ident
                }
            })
            .collect::<Vec<_>>();
        quote! {
            fn eq_props(&self, rhs: &Self::Props) -> bool {
                let inner = &*self.inner.borrow();
                #(#comparisons) && *
            }
        }
    } else {
        quote! {
            fn eq_props(&self, _: &Self::Props) -> bool {
                // all props are eq if there are no props
                true
            }
        }
    }
}

fn impl_state_setters_and_notifier(state: &Ident, fields: &Fields) -> Tokens {
    match *fields {
        Fields::Named(ref named_fields) => {
            let named = &named_fields.named;
            let mut setters = vec![];
            for field in named.iter() {
                if !is_prop_field(field) {
                    let ident = field.ident.as_ref().unwrap();
                    let fn_name = Ident::from(format!("set_{}", ident));
                    let ty = &field.ty;
                    setters.push(
                        quote! {
                            #[allow(dead_code)]
                            fn #fn_name(&mut self, value: #ty) {
                                self.#ident = value;
                                self.notify();
                            }
                        }
                    );
                }
            }
            quote! {
                impl #state {
                    #(#setters)*

                    #[allow(dead_code)]
                    fn notify(&self) {
                        let notify = &self.notifier;
                        notify();
                    }
                }
            }
        }
        Fields::Unnamed(_) => {
            panic!("Tuple structs are not supported as components");
        }
        Fields::Unit => {
            quote!()
        }
    }
}

fn generate_props_struct(ident: &Ident, fields: &Fields) -> Tokens {
    let props = get_props_from_fields(fields);
    let field_tokens = props.iter().map(|it| {
        let ident = it.ident;
        let ty = &it.ty;
        quote! {
            #ident: #ty
        }
    }).collect::<Vec<_>>();
    if !field_tokens.is_empty() {
        quote! {
            #[derive(PartialEq)]
            struct #ident {
                #(#field_tokens),*
            }
        }
    } else {
        quote!()
    }
}

fn get_props_from_fields(fields: &Fields) -> Vec<Field> {
    match *fields {
        Fields::Named(ref named_fields) => {
            let mut list = vec![];
            for field in named_fields.named.iter() {
                if is_prop_field(field) {
                    list.push(field.clone());
                }
            }
            list
        }
        Fields::Unnamed(_) => {
            panic!("Tuple structs are not supported as components");
        }
        Fields::Unit => {
            Vec::new()
        }
    }
}

fn is_prop_field(field: &Field) -> bool {
    let prop_path = Path::from(Ident::from("prop".to_string()));
    field.attrs.iter().any(|it| it.path == prop_path)
}