From e0a428a8f9186a4590acb2b3771e50290aacbde1 Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Fri, 22 Jul 2022 22:38:58 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 + Cargo.toml | 5 ++ seed_router/Cargo.toml | 10 +++ seed_router/src/lib.rs | 19 ++++ seed_router_derive/Cargo.toml | 19 ++++ seed_router_derive/src/lib.rs | 158 ++++++++++++++++++++++++++++++++++ 6 files changed, 213 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 seed_router/Cargo.toml create mode 100644 seed_router/src/lib.rs create mode 100644 seed_router_derive/Cargo.toml create mode 100644 seed_router_derive/src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1b338fc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "seed_router", + "seed_router_derive", +] diff --git a/seed_router/Cargo.toml b/seed_router/Cargo.toml new file mode 100644 index 0000000..743ae1e --- /dev/null +++ b/seed_router/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "seed_router" +version = "0.1.0" +edition = "2021" + +[dependencies] +seed = "*" + +[dependencies.seed_router_derive] +path = "../seed_router_derive" diff --git a/seed_router/src/lib.rs b/seed_router/src/lib.rs new file mode 100644 index 0000000..9a93cdb --- /dev/null +++ b/seed_router/src/lib.rs @@ -0,0 +1,19 @@ +use seed::prelude::{Node, Orders, Url}; + +pub use seed_router_derive::Router; + +pub trait Page { + type Msg: 'static; + fn new(orders: &mut impl Orders) -> Self; + fn update(&mut self, msg: Self::Msg, orders: &mut impl Orders); + fn view(&self) -> Node; +} + +pub trait Router: Sized { + type Msg: 'static; + fn from_url(url: Url, orders: &mut impl Orders) -> Option; + fn update(&mut self, msg: Self::Msg, orders: &mut impl Orders); + fn view(&self) -> Node; +} + +pub const fn assert_impl_page(_: &impl Page) {} diff --git a/seed_router_derive/Cargo.toml b/seed_router_derive/Cargo.toml new file mode 100644 index 0000000..c96142a --- /dev/null +++ b/seed_router_derive/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "seed_router_derive" +version = "0.1.0" +authors = ["Joakim Hulthe "] +edition = "2021" + +[lib] +proc_macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +regex = "1" +serde = "1" +seed = "*" + +[dependencies.syn] +version = "1" +features = ["derive", "parsing", "extra-traits"] diff --git a/seed_router_derive/src/lib.rs b/seed_router_derive/src/lib.rs new file mode 100644 index 0000000..ce83791 --- /dev/null +++ b/seed_router_derive/src/lib.rs @@ -0,0 +1,158 @@ +use proc_macro2::{Span, TokenStream}; +use quote::{quote, quote_spanned}; +use syn::{ + parse_macro_input, spanned::Spanned, Data, DeriveInput, Error, Ident, Lit, Meta, NestedMeta, +}; + +#[proc_macro_derive(Router, attributes(page))] +pub fn derive_router(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match inner_derive_router(input) { + Ok(tokens) => tokens.into(), + Err(e) => e.into_compile_error().into(), + } +} + +fn inner_derive_router(input: DeriveInput) -> syn::Result { + let ident = input.ident; + let data = match input.data { + Data::Enum(data) => data, + Data::Struct(_) => panic!("Struct not supported"), + Data::Union(_) => panic!("Union not supported"), + }; + + let mut pages = vec![]; + + // TODO + let msg_type = Ident::new("PageMsg", Span::call_site()); + + // parse enum + let page_attribute = Ident::new("page", Span::call_site()); + for punct in data.variants.into_iter() { + assert!( + punct.discriminant.is_none(), + "Enum discriminant not supported" + ); + + let ident = punct.ident; + let fields = punct.fields; + let attributes = punct.attrs; + let page_attribute = attributes + .iter() + .map(|attribute| attribute.parse_meta().expect("expected a string literatl")) + .filter_map(|meta| match meta { + Meta::Path(_) | Meta::NameValue(_) => None, + Meta::List(meta_list) => Some(meta_list), + }) + .find(|meta_list| meta_list.path.is_ident(&page_attribute)) + .ok_or_else(|| Error::new(ident.span(), "Missing \"page\" attribute"))?; + + let mut page_attribute_values = page_attribute.nested.iter(); + let page_location = match page_attribute_values.next() { + Some(NestedMeta::Lit(Lit::Str(literal))) => literal.value(), + Some(NestedMeta::Lit(literal)) => { + return Err(Error::new(literal.span(), "Expected string literal")) + } + Some(NestedMeta::Meta(meta)) => { + return Err(Error::new(meta.span(), "Expected string literal")) + } + None => { + return Err(Error::new( + page_attribute.span(), + "Missing attribute value: location", + )) + } + }; + + let msg_type = match page_attribute_values.next() { + Some(NestedMeta::Meta(Meta::Path(path))) => path + .get_ident() + .cloned() + .ok_or_else(|| Error::new(path.span(), "Expected Identifier"))?, + Some(NestedMeta::Meta(meta)) => { + return Err(Error::new(meta.span(), "Expected identifier")) + } + Some(NestedMeta::Lit(literal)) => { + return Err(Error::new(literal.span(), "Expected identifier")) + } + None => { + return Err(Error::new( + page_attribute.span(), + "Missing attribute value: Message Type", + )) + } + }; + + pages.push((ident, page_location, msg_type)); + } + + // generate implementation + let from_url_match_arms: TokenStream = pages + .iter() + .map(|(variant, page_location, msg_variant)| { + quote_spanned! { variant.span() => + [#page_location] => { + let mut orders = orders.proxy(#msg_type::#msg_variant); + Some(Self::#variant(::seed_router::Page::new(&mut orders))) + } + } + }) + .collect(); + + let update_match_arms: TokenStream = pages + .iter() + .map(|(variant, _page_location, msg_variant)| { + quote_spanned! { variant.span() => + Self::#variant(page) => { + ::seed_router::assert_impl_page(page); + if let #msg_type::#msg_variant(msg) = msg { + let mut orders = orders.proxy(#msg_type::#msg_variant); + page.update(msg, &mut orders); + } + } + } + }) + .collect(); + + let view_match_arms: TokenStream = pages + .iter() + .map(|(variant, _page_location, msg_variant)| { + quote_spanned! { variant.span() => + Self::#variant(page) => page.view().map_msg(#msg_type::#msg_variant), + } + }) + .collect(); + + let output = quote! { + impl ::seed_router::Router for #ident { + type Msg = #msg_type; + + fn from_url( + mut url: ::seed::prelude::Url, + orders: &mut impl ::seed::prelude::Orders, + ) -> Option { + use ::seed_router::Page as _; + match url.remaining_path_parts().as_slice() { + #from_url_match_arms + _ => None, + } + } + + fn update(&mut self, msg: Self::Msg, orders: &mut impl ::seed::prelude::Orders) { + use ::seed_router::Page as _; + match self { + #update_match_arms + } + } + + fn view(&self) -> ::seed::prelude::Node { + use ::seed_router::Page as _; + match self { + #view_match_arms + } + } + } + }; + + Ok(output) +}