Skip to content

Commit e81ca42

Browse files
Merge pull request askama-rs#255 from Kijewski/pr-enum
Implement `enum` variants
2 parents e418834 + 1066c88 commit e81ca42

File tree

8 files changed

+725
-45
lines changed

8 files changed

+725
-45
lines changed

.github/workflows/rust.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ jobs:
172172
with:
173173
tool: cargo-nextest
174174
- uses: Swatinem/rust-cache@v2
175-
- run: cd ${{ matrix.package }} && cargo nextest run --no-tests=warn
175+
- run: cd ${{ matrix.package }} && cargo build --all-targets
176+
- run: cd ${{ matrix.package }} && cargo nextest run --all-targets --no-fail-fast --no-tests=warn
176177
- run: cd ${{ matrix.package }} && cargo clippy --all-targets -- -D warnings
177178

178179
MSRV:

book/src/creating_templates.md

+79
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,85 @@ recognized:
118118
struct HelloTemplate<'a> { ... }
119119
```
120120

121+
## Templating `enum`s
122+
123+
You can add derive `Template`s for `struct`s and `enum`s.
124+
If you add `#[template()]` only to the item itself, both item kinds work exactly the same.
125+
But with `enum`s you also have the option to add a specialized implementation to one, some,
126+
or all variants:
127+
128+
```rust
129+
#[derive(Debug, Template)]
130+
#[template(path = "area.txt")]
131+
enum Area {
132+
Square(f32),
133+
Rectangle { a: f32, b: f32 },
134+
Circle { radius: f32 },
135+
}
136+
```
137+
138+
```jinja2
139+
{%- match self -%}
140+
{%- when Self::Square(side) -%}
141+
{{side}}^2
142+
{%- when Self::Rectangle { a, b} -%}
143+
{{a}} * {{b}}
144+
{%- when Self::Circle { radius } -%}
145+
pi * {{radius}}^2
146+
{%- endmatch -%}
147+
```
148+
149+
will give you the same results as:
150+
151+
```rust
152+
#[derive(Template, Debug)]
153+
#[template(ext = "txt")]
154+
enum AreaPerVariant {
155+
#[template(source = "{{self.0}}^2")]
156+
Square(f32),
157+
#[template(source = "{{a}} * {{b}}")]
158+
Rectangle { a: f32, b: f32 },
159+
#[template(source = "pi * {{radius}}^2")]
160+
Circle { radius: f32 },
161+
}
162+
```
163+
164+
As you can see with the `ext` attribute, `enum` variants inherit most settings of the `enum`:
165+
`config`, `escape`, `ext`, `syntax`, and `whitespace`.
166+
Not inherited are: `block`, and `print`.
167+
168+
If there is no `#[template]` annotation for an `enum` variant,
169+
then the `enum` needs a default implementation, which will be used if `self` is this variant.
170+
A good compromise between annotating only the template, or all its variants,
171+
might be using the `block` argument on the members:
172+
173+
```rust
174+
#[derive(Template, Debug)]
175+
#[template(path = "area.txt")]
176+
enum AreaWithBlocks {
177+
#[template(block = "square")]
178+
Square(f32),
179+
#[template(block = "rectangle")]
180+
Rectangle { a: f32, b: f32 },
181+
#[template(block = "circle")]
182+
Circle { radius: f32 },
183+
}
184+
```
185+
186+
```jinja2
187+
{%- block square -%}
188+
{{self.0}}^2
189+
{%- endblock -%}
190+
191+
{%- block rectangle -%}
192+
{{a}} * {{b}}
193+
{%- endblock -%}
194+
195+
{%- block circle -%}
196+
pi * {{radius}}^2
197+
{%- endblock -%}
198+
```
199+
121200
## Documentation as template code
122201
[#documentation-as-template-code]: #documentation-as-template-code
123202

rinja/src/helpers.rs

+8
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,11 @@ impl<L: FastWritable, R: FastWritable> FastWritable for Concat<L, R> {
269269
self.1.write_into(dest)
270270
}
271271
}
272+
273+
pub trait EnumVariantTemplate {
274+
fn render_into_with_values<W: fmt::Write + ?Sized>(
275+
&self,
276+
writer: &mut W,
277+
values: &dyn crate::Values,
278+
) -> crate::Result<()>;
279+
}

rinja_derive/src/generator.rs

+37-33
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ pub(crate) fn template_to_string(
2525
input: &TemplateInput<'_>,
2626
contexts: &HashMap<&Arc<Path>, Context<'_>, FxBuildHasher>,
2727
heritage: Option<&Heritage<'_, '_>>,
28-
target: Option<&str>,
28+
tmpl_kind: TmplKind,
2929
) -> Result<usize, CompileError> {
30-
let ctx = &contexts[&input.path];
30+
if tmpl_kind == TmplKind::Struct {
31+
buf.write("const _: () = { extern crate rinja as rinja;");
32+
}
33+
3134
let generator = Generator::new(
3235
input,
3336
contexts,
@@ -36,13 +39,27 @@ pub(crate) fn template_to_string(
3639
input.block.is_some(),
3740
0,
3841
);
39-
let mut result = generator.build(ctx, buf, target);
40-
if let Err(err) = &mut result {
41-
if err.span.is_none() {
42+
let size_hint = match generator.impl_template(buf, tmpl_kind) {
43+
Err(mut err) if err.span.is_none() => {
4244
err.span = input.source_span;
45+
Err(err)
4346
}
47+
result => result,
48+
}?;
49+
50+
if tmpl_kind == TmplKind::Struct {
51+
impl_everything(input.ast, buf);
52+
buf.write("};");
4453
}
45-
result
54+
Ok(size_hint)
55+
}
56+
57+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58+
pub(crate) enum TmplKind {
59+
/// [`rinja::Template`]
60+
Struct,
61+
/// [`rinja::helpers::EnumVariantTemplate`]
62+
Variant,
4663
}
4764

4865
struct Generator<'a, 'h> {
@@ -97,31 +114,18 @@ impl<'a, 'h> Generator<'a, 'h> {
97114
}
98115
}
99116

100-
// Takes a Context and generates the relevant implementations.
101-
fn build(
102-
mut self,
103-
ctx: &Context<'a>,
104-
buf: &mut Buffer,
105-
target: Option<&str>,
106-
) -> Result<usize, CompileError> {
107-
if target.is_none() {
108-
buf.write("const _: () = { extern crate rinja as rinja;");
109-
}
110-
let size_hint = self.impl_template(ctx, buf, target.unwrap_or("rinja::Template"))?;
111-
if target.is_none() {
112-
impl_everything(self.input.ast, buf);
113-
buf.write("};");
114-
}
115-
Ok(size_hint)
116-
}
117-
118117
// Implement `Template` for the given context struct.
119118
fn impl_template(
120-
&mut self,
121-
ctx: &Context<'a>,
119+
mut self,
122120
buf: &mut Buffer,
123-
target: &str,
121+
tmpl_kind: TmplKind,
124122
) -> Result<usize, CompileError> {
123+
let ctx = &self.contexts[&self.input.path];
124+
125+
let target = match tmpl_kind {
126+
TmplKind::Struct => "rinja::Template",
127+
TmplKind::Variant => "rinja::helpers::EnumVariantTemplate",
128+
};
125129
write_header(self.input.ast, buf, target);
126130
buf.write(
127131
"fn render_into_with_values<RinjaW>(\
@@ -161,12 +165,12 @@ impl<'a, 'h> Generator<'a, 'h> {
161165

162166
let size_hint = self.impl_template_inner(ctx, buf)?;
163167

164-
buf.write(format_args!(
165-
"\
166-
rinja::Result::Ok(())\
167-
}}\
168-
const SIZE_HINT: rinja::helpers::core::primitive::usize = {size_hint}usize;",
169-
));
168+
buf.write("rinja::Result::Ok(()) }");
169+
if tmpl_kind == TmplKind::Struct {
170+
buf.write(format_args!(
171+
"const SIZE_HINT: rinja::helpers::core::primitive::usize = {size_hint}usize;",
172+
));
173+
}
170174

171175
buf.write('}');
172176
Ok(size_hint)

rinja_derive/src/input.rs

+68
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,63 @@ impl TemplateInput<'_> {
271271
}
272272
}
273273

274+
pub(crate) enum AnyTemplateArgs {
275+
Struct(TemplateArgs),
276+
Enum {
277+
enum_args: Option<PartialTemplateArgs>,
278+
vars_args: Vec<Option<PartialTemplateArgs>>,
279+
has_default_impl: bool,
280+
},
281+
}
282+
283+
impl AnyTemplateArgs {
284+
pub(crate) fn new(ast: &syn::DeriveInput) -> Result<Self, CompileError> {
285+
let syn::Data::Enum(enum_data) = &ast.data else {
286+
return Ok(Self::Struct(TemplateArgs::new(ast)?));
287+
};
288+
289+
let enum_args = PartialTemplateArgs::new(ast, &ast.attrs)?;
290+
let vars_args = enum_data
291+
.variants
292+
.iter()
293+
.map(|variant| PartialTemplateArgs::new(ast, &variant.attrs))
294+
.collect::<Result<Vec<_>, _>>()?;
295+
if vars_args.is_empty() {
296+
return Ok(Self::Struct(TemplateArgs::from_partial(ast, enum_args)?));
297+
}
298+
299+
let mut needs_default_impl = vars_args.len();
300+
let enum_source = enum_args.as_ref().and_then(|v| v.source.as_ref());
301+
for (variant, var_args) in enum_data.variants.iter().zip(&vars_args) {
302+
if var_args
303+
.as_ref()
304+
.and_then(|v| v.source.as_ref())
305+
.or(enum_source)
306+
.is_none()
307+
{
308+
return Err(CompileError::new_with_span(
309+
#[cfg(not(feature = "code-in-doc"))]
310+
"either all `enum` variants need a `path` or `source` argument, \
311+
or the `enum` itself needs a default implementation",
312+
#[cfg(feature = "code-in-doc")]
313+
"either all `enum` variants need a `path`, `source` or `in_doc` argument, \
314+
or the `enum` itself needs a default implementation",
315+
None,
316+
Some(variant.ident.span()),
317+
));
318+
} else if !var_args.is_none() {
319+
needs_default_impl -= 1;
320+
}
321+
}
322+
323+
Ok(Self::Enum {
324+
enum_args,
325+
vars_args,
326+
has_default_impl: needs_default_impl > 0,
327+
})
328+
}
329+
}
330+
274331
#[derive(Debug)]
275332
pub(crate) struct TemplateArgs {
276333
pub(crate) source: (Source, Option<Span>),
@@ -626,6 +683,17 @@ pub(crate) enum PartialTemplateArgsSource {
626683
InDoc(Span, Source),
627684
}
628685

686+
impl PartialTemplateArgsSource {
687+
pub(crate) fn span(&self) -> Span {
688+
match self {
689+
Self::Path(s) => s.span(),
690+
Self::Source(s) => s.span(),
691+
#[cfg(feature = "code-in-doc")]
692+
Self::InDoc(s, _) => s.span(),
693+
}
694+
}
695+
}
696+
629697
// implement PartialTemplateArgs::new()
630698
const _: () = {
631699
impl PartialTemplateArgs {

0 commit comments

Comments
 (0)