From ad463d59d8844f0c97c1b64737f5bf550b81f0c1 Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Tue, 1 Jun 2021 14:52:33 -0700 Subject: [PATCH 1/7] add inline method call syntax --- mox/src/lib.rs | 154 ++++++++++++++++++++++++++++--------- mox/tests/method_syntax.rs | 68 ++++++++++++++++ 2 files changed, 184 insertions(+), 38 deletions(-) create mode 100644 mox/tests/method_syntax.rs diff --git a/mox/src/lib.rs b/mox/src/lib.rs index 528ac525..72af6509 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,30 @@ 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 MoxBlock { + pub fn parse(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 +191,17 @@ 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::parse(parse_stream).map_err(|e| {eprintln!("MoxBlock failed"); e})? { + 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) + .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 +315,12 @@ impl ToTokens for MoxTag { } } -struct MoxAttr { +enum MoxAttr { + MethodCall(syn::ExprCall), + KeyValue { name: syn::Ident, value: Option, + } } impl TryFrom for MoxAttr { @@ -289,20 +328,47 @@ 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 }) + Ok(MoxAttr::KeyValue { name: MoxAttr::validate_name(node.name.unwrap())?, value: node.value }) } } } } 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::{Expr, ExprBlock, Error, Stmt, token::Semi}; + + 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. `foo()`")), + }; + + 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(|call| MoxAttr::MethodCall(call)) + } + fn validate_name(name: syn_rsx::NodeName) -> syn::Result { use syn::{punctuated::Pair, PathSegment}; @@ -338,11 +404,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: Some(value)} => quote!(.#name(#value)), + Self::KeyValue {name, value: None} => quote!(.#name(#name)), + Self::MethodCall(call) => quote!(.#call), }; + + tokens.extend(call); } } @@ -423,20 +491,30 @@ 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! { }); + 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!(); + } - 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!(); } diff --git a/mox/tests/method_syntax.rs b/mox/tests/method_syntax.rs new file mode 100644 index 00000000..6cfc2ab9 --- /dev/null +++ b/mox/tests/method_syntax.rs @@ -0,0 +1,68 @@ +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 + ); +} From ad7cea70a499618646cb672e4d53a46b6a6bfab9 Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Tue, 1 Jun 2021 15:02:08 -0700 Subject: [PATCH 2/7] add punned variant to moxattr --- mox/src/lib.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/mox/src/lib.rs b/mox/src/lib.rs index 72af6509..8a784ad2 100644 --- a/mox/src/lib.rs +++ b/mox/src/lib.rs @@ -317,9 +317,10 @@ impl ToTokens for MoxTag { enum MoxAttr { MethodCall(syn::ExprCall), + Punned(syn::Ident), KeyValue { name: syn::Ident, - value: Option, + value: syn::Expr, } } @@ -335,7 +336,12 @@ impl TryFrom for MoxAttr { | NodeType::Doctype | NodeType::Fragment => Err(Self::node_convert_error(&node)), NodeType::Attribute => { - Ok(MoxAttr::KeyValue { name: MoxAttr::validate_name(node.name.unwrap())?, value: node.value }) + let name = MoxAttr::validate_name(node.name.unwrap())?; + let attr = node.value + .map(|value| MoxAttr::KeyValue{ name: name.clone(), value }) + .unwrap_or_else(|| MoxAttr::Punned(name)); + + Ok(attr) } } } @@ -405,8 +411,8 @@ impl MoxAttr { impl ToTokens for MoxAttr { fn to_tokens(&self, tokens: &mut TokenStream) { let call = match self { - Self::KeyValue {name, value: Some(value)} => quote!(.#name(#value)), - Self::KeyValue {name, value: None} => quote!(.#name(#name)), + Self::KeyValue {name, value} => quote!(.#name(#value)), + Self::Punned(name) => quote!(.#name(#name)), Self::MethodCall(call) => quote!(.#call), }; From a05faa6a5ad0d7c9eb01e1a0c4d67c38dcc800aa Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Tue, 1 Jun 2021 15:05:28 -0700 Subject: [PATCH 3/7] change inherent method to tryfrom impl --- mox/src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mox/src/lib.rs b/mox/src/lib.rs index 8a784ad2..d8b07358 100644 --- a/mox/src/lib.rs +++ b/mox/src/lib.rs @@ -166,8 +166,10 @@ enum MoxBlock { Block, } -impl MoxBlock { - pub fn parse(parse_stream: ParseStream) -> syn::Result { +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 = @@ -192,7 +194,7 @@ enum MoxItem { impl Parse for MoxItem { fn parse(input: ParseStream) -> syn::Result { fn parse_block(parse_stream: ParseStream) -> syn::Result> { - match MoxBlock::parse(parse_stream).map_err(|e| {eprintln!("MoxBlock failed"); e})? { + match MoxBlock::try_from(parse_stream)? { MoxBlock::Block => Ok(None), MoxBlock::FormatExpr(arguments) => Ok(Some(quote!(format_args!(#arguments)))) } From e8159125f36a5278f507cce29418f96d0fff2512 Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Tue, 1 Jun 2021 15:09:10 -0700 Subject: [PATCH 4/7] more readable --- mox/src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mox/src/lib.rs b/mox/src/lib.rs index d8b07358..689746ba 100644 --- a/mox/src/lib.rs +++ b/mox/src/lib.rs @@ -339,9 +339,11 @@ impl TryFrom for MoxAttr { | NodeType::Fragment => Err(Self::node_convert_error(&node)), NodeType::Attribute => { let name = MoxAttr::validate_name(node.name.unwrap())?; - let attr = node.value - .map(|value| MoxAttr::KeyValue{ name: name.clone(), value }) - .unwrap_or_else(|| MoxAttr::Punned(name)); + + let attr = match node.value { + Some(value) => MoxAttr::KeyValue { name: name.clone(), value }, + None => MoxAttr::Punned(name), + }; Ok(attr) } From 7fc8c236d269136199e4c09bc8a081ba18e2b4de Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Tue, 1 Jun 2021 15:10:45 -0700 Subject: [PATCH 5/7] improve error message example --- mox/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mox/src/lib.rs b/mox/src/lib.rs index 689746ba..a2524a9c 100644 --- a/mox/src/lib.rs +++ b/mox/src/lib.rs @@ -369,7 +369,7 @@ impl MoxAttr { 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. `foo()`")), + _ => Err(Error::new(node_span(&node), "Only method calls are supported in the attribute position.\ne.g. ``")), }; node.value_as_block() From f2ccecbb0972f023e139a3aa3df8120dce8c12d7 Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Tue, 1 Jun 2021 15:13:13 -0700 Subject: [PATCH 6/7] fmt --- mox/src/lib.rs | 128 +++++++++++++++++++------------------ mox/tests/method_syntax.rs | 18 ++---- 2 files changed, 69 insertions(+), 77 deletions(-) diff --git a/mox/src/lib.rs b/mox/src/lib.rs index a2524a9c..02b1130d 100644 --- a/mox/src/lib.rs +++ b/mox/src/lib.rs @@ -160,10 +160,10 @@ pub fn mox(input: proc_macro::TokenStream) -> proc_macro::TokenStream { } enum MoxBlock { - /// {% ...format_args } - FormatExpr(Punctuated), - /// Arbitrary Rust expression - Block, + /// {% ...format_args } + FormatExpr(Punctuated), + /// Arbitrary Rust expression + Block, } impl<'a> TryFrom> for MoxBlock { @@ -196,13 +196,12 @@ impl Parse for MoxItem { 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)))) + MoxBlock::FormatExpr(arguments) => Ok(Some(quote!(format_args!(#arguments)))), } } - let parse_config = syn_rsx::ParserConfig::new() - .transform_block(parse_block) - .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); @@ -318,12 +317,9 @@ impl ToTokens for MoxTag { } enum MoxAttr { - MethodCall(syn::ExprCall), - Punned(syn::Ident), - KeyValue { - name: syn::Ident, - value: syn::Expr, - } + MethodCall(syn::ExprCall), + Punned(syn::Ident), + KeyValue { name: syn::Ident, value: syn::Expr }, } impl TryFrom for MoxAttr { @@ -355,28 +351,35 @@ 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::{Expr, ExprBlock, Error, Stmt, token::Semi}; - - 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(|call| MoxAttr::MethodCall(call)) + 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(|call| MoxAttr::MethodCall(call)) } fn validate_name(name: syn_rsx::NodeName) -> syn::Result { @@ -415,9 +418,9 @@ impl MoxAttr { impl ToTokens for MoxAttr { fn to_tokens(&self, tokens: &mut TokenStream) { let call = match self { - Self::KeyValue {name, value} => quote!(.#name(#value)), - Self::Punned(name) => quote!(.#name(#name)), - Self::MethodCall(call) => quote!(.#call), + Self::KeyValue { name, value } => quote!(.#name(#value)), + Self::Punned(name) => quote!(.#name(#name)), + Self::MethodCall(call) => quote!(.#call), }; tokens.extend(call); @@ -502,29 +505,28 @@ fn node_span(node: &syn_rsx::Node) -> Span { #[cfg(test)] 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! { }); - 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!(); - } + 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! { }); + 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 index 6cfc2ab9..9551660e 100644 --- a/mox/tests/method_syntax.rs +++ b/mox/tests/method_syntax.rs @@ -30,16 +30,12 @@ impl TagBuilder { } fn optional(mut self) -> Self { - self.optional = true; - self + self.optional = true; + self } fn build(self) -> Tag { - Tag { - name: self.name.unwrap(), - children: self.children, - optional: self.optional, - } + Tag { name: self.name.unwrap(), children: self.children, optional: self.optional } } } @@ -47,13 +43,7 @@ impl TagBuilder { fn method_syntax() { let expected = Tag { name: String::from("alice"), - children: vec![ - Tag { - name: String::from("bob"), - children: vec![], - optional: false, - } - ], + children: vec![Tag { name: String::from("bob"), children: vec![], optional: false }], optional: true, }; From f209cb8e8b8d7cc4a23a5b79fda85b345e715b03 Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Tue, 1 Jun 2021 15:14:20 -0700 Subject: [PATCH 7/7] clippy --- mox/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mox/src/lib.rs b/mox/src/lib.rs index 02b1130d..60538a1e 100644 --- a/mox/src/lib.rs +++ b/mox/src/lib.rs @@ -337,7 +337,7 @@ impl TryFrom for MoxAttr { let name = MoxAttr::validate_name(node.name.unwrap())?; let attr = match node.value { - Some(value) => MoxAttr::KeyValue { name: name.clone(), value }, + Some(value) => MoxAttr::KeyValue { name, value }, None => MoxAttr::Punned(name), }; @@ -379,7 +379,7 @@ impl MoxAttr { }) .and_then(try_get_stmt) .and_then(try_get_call) - .map(|call| MoxAttr::MethodCall(call)) + .map(MoxAttr::MethodCall) } fn validate_name(name: syn_rsx::NodeName) -> syn::Result {