diff --git a/mox/src/lib.rs b/mox/src/lib.rs index 528ac525..60538a1e 100644 --- a/mox/src/lib.rs +++ b/mox/src/lib.rs @@ -58,6 +58,13 @@ use syn_rsx::{punctuation::Dash, NodeName, NodeType}; /// If the attribute's name is `async`, `for`, `loop`, or `type` an underscore /// is appended to avoid colliding with the Rust keyword. /// +/// #### Alternate syntax +/// +/// To allow calling methods with 0 or more than 1 arguments, +/// an inline method call syntax is available +/// +/// e.g. `` expands to `foo().bar().baz(123).build()` +/// /// ### Children /// /// Tags have zero or more nested items (tags, fragments, content) as children. @@ -90,6 +97,7 @@ use syn_rsx::{punctuation::Dash, NodeName, NodeType}; /// #[derive(Debug, PartialEq)] /// struct Tag { /// name: String, +/// is_optional: bool, /// children: Vec, /// } /// @@ -100,6 +108,7 @@ use syn_rsx::{punctuation::Dash, NodeName, NodeType}; /// #[derive(Default)] /// struct TagBuilder { /// name: Option, +/// is_optional: bool, /// children: Vec, /// } /// @@ -114,21 +123,31 @@ use syn_rsx::{punctuation::Dash, NodeName, NodeType}; /// self /// } /// +/// fn optional(mut self) -> Self { +/// self.is_optional = true; +/// self +/// } +/// /// fn build(self) -> Tag { -/// Tag { name: self.name.unwrap(), children: self.children } +/// Tag { +/// name: self.name.unwrap(), +/// children: self.children, +/// is_optional: self.is_optional, +/// } /// } /// } /// /// assert_eq!( /// mox! { -/// +/// /// /// /// /// }, /// Tag { /// name: String::from("alice"), -/// children: vec![Tag { name: String::from("bob"), children: vec![] }], +/// is_optional: true, +/// children: vec![Tag { name: String::from("bob"), is_optional: false, children: vec![] }], /// }, /// ); /// ``` @@ -140,6 +159,32 @@ pub fn mox(input: proc_macro::TokenStream) -> proc_macro::TokenStream { quote!(#item .build()).into() } +enum MoxBlock { + /// {% ...format_args } + FormatExpr(Punctuated), + /// Arbitrary Rust expression + Block, +} + +impl<'a> TryFrom> for MoxBlock { + type Error = syn::Error; + + fn try_from(parse_stream: ParseStream) -> syn::Result { + if parse_stream.peek(syn::Token![%]) { + parse_stream.parse::()?; + let arguments: Punctuated = + Punctuated::parse_separated_nonempty(parse_stream)?; + if parse_stream.is_empty() { + Ok(MoxBlock::FormatExpr(arguments)) + } else { + Err(parse_stream.error(format!("Expected the end, found `{}`", parse_stream))) + } + } else { + Ok(MoxBlock::Block) + } + } +} + enum MoxItem { Tag(MoxTag), Expr(MoxExpr), @@ -148,24 +193,16 @@ enum MoxItem { impl Parse for MoxItem { fn parse(input: ParseStream) -> syn::Result { - fn parse_fmt_expr(parse_stream: ParseStream) -> syn::Result> { - if parse_stream.peek(syn::Token![%]) { - parse_stream.parse::()?; - let arguments: Punctuated = - Punctuated::parse_separated_nonempty(parse_stream)?; - if parse_stream.is_empty() { - Ok(Some(quote!(format_args!(#arguments)))) - } else { - Err(parse_stream.error(format!("Expected the end, found `{}`", parse_stream))) - } - } else { - Ok(None) + fn parse_block(parse_stream: ParseStream) -> syn::Result> { + match MoxBlock::try_from(parse_stream)? { + MoxBlock::Block => Ok(None), + MoxBlock::FormatExpr(arguments) => Ok(Some(quote!(format_args!(#arguments)))), } } - let parse_config = syn_rsx::ParserConfig::new() - .transform_block(parse_fmt_expr) - .number_of_top_level_nodes(1); + let parse_config = + syn_rsx::ParserConfig::new().transform_block(parse_block).number_of_top_level_nodes(1); + let parser = syn_rsx::Parser::new(parse_config); let node = parser.parse(input)?.remove(0); @@ -279,9 +316,10 @@ impl ToTokens for MoxTag { } } -struct MoxAttr { - name: syn::Ident, - value: Option, +enum MoxAttr { + MethodCall(syn::ExprCall), + Punned(syn::Ident), + KeyValue { name: syn::Ident, value: syn::Expr }, } impl TryFrom for MoxAttr { @@ -289,20 +327,61 @@ impl TryFrom for MoxAttr { fn try_from(node: syn_rsx::Node) -> syn::Result { match node.node_type { + NodeType::Block => Self::try_parse_method_syntax(node), NodeType::Element | NodeType::Text - | NodeType::Block | NodeType::Comment | NodeType::Doctype | NodeType::Fragment => Err(Self::node_convert_error(&node)), NodeType::Attribute => { - Ok(MoxAttr { name: MoxAttr::validate_name(node.name.unwrap())?, value: node.value }) + let name = MoxAttr::validate_name(node.name.unwrap())?; + + let attr = match node.value { + Some(value) => MoxAttr::KeyValue { name, value }, + None => MoxAttr::Punned(name), + }; + + Ok(attr) } } } } impl MoxAttr { + /// Parse inline method call syntax, e.g. + /// `` -> `foo().bar().build()` + fn try_parse_method_syntax(node: syn_rsx::Node) -> syn::Result { + use syn::{token::Semi, Error, Expr, ExprBlock, Stmt}; + + let try_get_stmt = |mut block: ExprBlock| { + if block.block.stmts.len() == 1 { + Ok(block.block.stmts.pop().unwrap()) + } else { + Err(syn::Error::new( + node_span(&node), + "method syntax must only contain a single statement.", + )) + } + }; + + let try_get_call = |stmt: Stmt| match stmt { + Stmt::Expr(Expr::Call(call)) => Ok(call), + Stmt::Semi(_, Semi { spans: [semi] }) => Err(Error::new(semi, "Remove this semicolon")), + _ => Err(Error::new( + node_span(&node), + "Only method calls are supported in the attribute position.\ne.g. ``", + )), + }; + + node.value_as_block() + .ok_or_else(|| { + unreachable!("`try_parse_method_syntax` should only ever be called on block nodes.") + }) + .and_then(try_get_stmt) + .and_then(try_get_call) + .map(MoxAttr::MethodCall) + } + fn validate_name(name: syn_rsx::NodeName) -> syn::Result { use syn::{punctuated::Pair, PathSegment}; @@ -338,11 +417,13 @@ impl MoxAttr { impl ToTokens for MoxAttr { fn to_tokens(&self, tokens: &mut TokenStream) { - let Self { name, value } = self; - match value { - Some(value) => tokens.extend(quote!(.#name(#value))), - None => tokens.extend(quote!(.#name(#name))), + let call = match self { + Self::KeyValue { name, value } => quote!(.#name(#value)), + Self::Punned(name) => quote!(.#name(#name)), + Self::MethodCall(call) => quote!(.#call), }; + + tokens.extend(call); } } @@ -423,20 +504,29 @@ fn node_span(node: &syn_rsx::Node) -> Span { } #[cfg(test)] -#[test] -fn fails() { - fn assert_error(input: TokenStream) { - match syn::parse2::(input) { - Ok(_) => unreachable!(), - Err(error) => println!("{}", error), +mod tests { + use super::*; + + #[test] + fn fails() { + fn assert_error(input: TokenStream) { + match syn::parse2::(input) { + Ok(_) => unreachable!(), + Err(error) => println!("{}", error), + } } - } - println!(); - assert_error(quote! { }); - assert_error(quote! { <{"block tag name"} /> }); - assert_error(quote! { }); - assert_error(quote! { }); - assert_error(quote! { {% "1: {}; 2: {}", var1, var2 tail } }); - println!(); + println!(); + assert_error(quote! { }); + assert_error(quote! { }); + assert_error(quote! { }); + assert_error(quote! { }); + assert_error(quote! { }); + assert_error(quote! { }); + assert_error(quote! { <{"block tag name"} /> }); + assert_error(quote! { }); + assert_error(quote! { }); + assert_error(quote! { {% "1: {}; 2: {}", var1, var2 tail } }); + println!(); + } } diff --git a/mox/tests/method_syntax.rs b/mox/tests/method_syntax.rs new file mode 100644 index 00000000..9551660e --- /dev/null +++ b/mox/tests/method_syntax.rs @@ -0,0 +1,58 @@ +use mox::mox; + +#[derive(Debug, PartialEq)] +struct Tag { + name: String, + children: Vec, + optional: bool, +} + +fn built() -> TagBuilder { + TagBuilder::default() +} + +#[derive(Default)] +struct TagBuilder { + name: Option, + children: Vec, + optional: bool, +} + +impl TagBuilder { + fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + fn child(mut self, child: TagBuilder) -> Self { + self.children.push(child.build()); + self + } + + fn optional(mut self) -> Self { + self.optional = true; + self + } + + fn build(self) -> Tag { + Tag { name: self.name.unwrap(), children: self.children, optional: self.optional } + } +} + +#[test] +fn method_syntax() { + let expected = Tag { + name: String::from("alice"), + children: vec![Tag { name: String::from("bob"), children: vec![], optional: false }], + optional: true, + }; + + assert_eq!( + mox! { + + + + }, + expected + ); +}