From f25dac9409c306ab0157925b780902517473e0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 7 Aug 2024 07:01:02 +0200 Subject: [PATCH] derive: allow split up template attributes In generated code or macros, it might be useful to emit multiple `#[template]` attributes. E.g. ```rust #[template(source = "Hello!")] #[template(ext = "txt")] struct Hello; ``` --- rinja_derive/src/input.rs | 89 ++++++++++++------- testing/tests/simple.rs | 11 ++- .../ui/duplicated_template_attribute.stderr | 8 +- testing/tests/ui/no_template_attribute.stderr | 2 +- 4 files changed, 71 insertions(+), 39 deletions(-) diff --git a/rinja_derive/src/input.rs b/rinja_derive/src/input.rs index ea21215ef..1757456d2 100644 --- a/rinja_derive/src/input.rs +++ b/rinja_derive/src/input.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::collections::hash_map::{Entry, HashMap}; use std::fs::read_to_string; +use std::iter::FusedIterator; use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; @@ -290,47 +291,45 @@ pub(crate) struct TemplateArgs { } impl TemplateArgs { - pub(crate) fn new(ast: &'_ syn::DeriveInput) -> Result { - // Check that an attribute called `template()` exists once and that it is + pub(crate) fn new(ast: &syn::DeriveInput) -> Result { + // Check that an attribute called `template()` exists at least once and that it is // the proper type (list). - let mut span = None; - let mut template_args = None; - for attr in &ast.attrs { - let path = &attr.path(); - if !path.is_ident("template") { - continue; - } - span = Some(path.span()); - match attr.parse_args_with(Punctuated::::parse_terminated) { - Ok(args) if template_args.is_none() => template_args = Some(args), - Ok(_) => { - return Err(CompileError::no_file_info( - "duplicated 'template' attribute", - span, - )); - } - Err(e) => { - return Err(CompileError::no_file_info( + let mut templates_attrs = ast + .attrs + .iter() + .filter(|attr| attr.path().is_ident("template")) + .peekable(); + let mut args = match templates_attrs.peek() { + Some(attr) => Self { + template_span: Some(attr.path().span()), + ..Self::default() + }, + None => { + return Err(CompileError::no_file_info( + "no attribute `template` found", + None, + )); + } + }; + let attrs = templates_attrs + .map(|attr| { + type Attrs = Punctuated; + match attr.parse_args_with(Attrs::parse_terminated) { + Ok(args) => Ok(args), + Err(e) => Err(CompileError::no_file_info( format!("unable to parse template arguments: {e}"), - span, - )); + Some(attr.path().span()), + )), } - }; - } - - let template_args = template_args - .ok_or_else(|| CompileError::no_file_info("no attribute 'template' found", None))?; + }) + .flat_map(ResultIter::from); - let mut args = Self { - template_span: span, - ..Self::default() - }; // Loop over the meta attributes and find everything that we // understand. Return a CompileError if something is not right. // `source` contains an enum that can represent `path` or `source`. - for item in &template_args { - let pair = match item { + for item in attrs { + let pair = match item? { syn::Meta::NameValue(pair) => pair, v => { return Err(CompileError::no_file_info( @@ -427,6 +426,30 @@ impl TemplateArgs { } } +struct ResultIter(Result>); + +impl From> for ResultIter { + fn from(value: Result) -> Self { + Self(match value { + Ok(i) => Ok(i.into_iter()), + Err(e) => Err(Some(e)), + }) + } +} + +impl Iterator for ResultIter { + type Item = Result; + + fn next(&mut self) -> Option { + match &mut self.0 { + Ok(iter) => Some(Ok(iter.next()?)), + Err(err) => Some(Err(err.take()?)), + } + } +} + +impl FusedIterator for ResultIter {} + fn source_or_path( name: &syn::Ident, value: &syn::ExprLit, diff --git a/testing/tests/simple.rs b/testing/tests/simple.rs index 0df5274fa..76ec14687 100644 --- a/testing/tests/simple.rs +++ b/testing/tests/simple.rs @@ -544,10 +544,19 @@ struct TestI16ToU8 { } #[test] -#[allow(clippy::needless_borrows_for_generic_args)] fn test_i16_to_u8() { assert_eq!(TestI16ToU8 { input: 0 }.to_string(), "0 0 0"); assert_eq!(TestI16ToU8 { input: 0x7f00 }.to_string(), "0 0 0"); assert_eq!(TestI16ToU8 { input: 255 }.to_string(), "255 255 255"); assert_eq!(TestI16ToU8 { input: -12345 }.to_string(), "199 199 199"); } + +#[derive(Template)] +#[template(source = "🙂")] +#[template(ext = "txt")] +struct SplitTemplateDeclaration; + +#[test] +fn test_split_template_declaration() { + assert_eq!(SplitTemplateDeclaration.to_string(), "🙂") +} diff --git a/testing/tests/ui/duplicated_template_attribute.stderr b/testing/tests/ui/duplicated_template_attribute.stderr index c7362973a..dc36f16a7 100644 --- a/testing/tests/ui/duplicated_template_attribute.stderr +++ b/testing/tests/ui/duplicated_template_attribute.stderr @@ -1,5 +1,5 @@ -error: duplicated 'template' attribute - --> tests/ui/duplicated_template_attribute.rs:8:3 +error: must specify `source` OR `path` exactly once + --> tests/ui/duplicated_template_attribute.rs:9:5 | -8 | #[template( - | ^^^^^^^^ +9 | source = "🙃", + | ^^^^^^ diff --git a/testing/tests/ui/no_template_attribute.stderr b/testing/tests/ui/no_template_attribute.stderr index fa215cfc1..1fa1f29aa 100644 --- a/testing/tests/ui/no_template_attribute.stderr +++ b/testing/tests/ui/no_template_attribute.stderr @@ -1,4 +1,4 @@ -error: no attribute 'template' found +error: no attribute `template` found --> tests/ui/no_template_attribute.rs:4:8 | 4 | struct NoTemplate;