Add filters |default, |assigned_or and |defined_or#425
Add filters |default, |assigned_or and |defined_or#425Kijewski merged 4 commits intoaskama-rs:masterfrom
|default, |assigned_or and |defined_or#425Conversation
|
I am not sure if Also, it would be possible to implement the trait for integers (testing if != 0), and Strings (testing if != ""). |
4af0d79 to
90c531c
Compare
|
Btw, no clue about a better for |
| </li><li> | ||
|
|
||
| ```jinja | ||
| {{ variable_or_expression | default(default_value, true) }} |
There was a problem hiding this comment.
This sort of parameter without a name makes it difficult to understand a template worth reading the template docs. I'd been inclined to either:
- Replace a naked bool parameter with an enum that self documents (that may look verbose here), or
- Replace a bool with a second named method. E.g. default_if_undefined might be a good name for the false branch. Alternatively default_if_none / err
There was a problem hiding this comment.
I added a line that {{ variable_or_expression | default(default_value, boolean = true) }} works, too.
There was a problem hiding this comment.
I'd still prefer explicit over implicit a bit. This is based on an assumption that the direction of askama is a jinja-like template library with natural rust idioms rather than a jinja compatible template library that adds rust conveniences. I'm not sure whether that assumption is correct however (intuitively I'd lean towards the former as being more useful)
As a single data point. Having not seen the jinja syntax for the default filter prior, even with the parameter named boolean in the CoPilot suggestion rather than the bare true, I still had no idea from reading it what that parameter meant or controlled seeing it for the first time. This suggests to me that it is poorly named in jinja, and something that we could make better pretty easily with good naming here.
Empty string seems reasonable as that's rationally returned when a string is not entered somewhere, but most of the time a zero value seems like it would be only 0 if you're using that as a flag value, which is not very rusty, so I'd perhaps implement default_if_empty for strings (and str, cow perhaps), but not default_if_zero unless there's an explicit obvious use case where it makes sense. |
I think implementing the type for integers is fine and useful. In many real-world cases you will find
|
| pub fn pluralize<C, S, P>(count: C, singular: S, plural: P) -> Result<Pluralize<S, P>, C::Error> | ||
| pub fn pluralize<C, S, P>(count: C, singular: S, plural: P) -> Result<Either<S, P>, C::Error> |
There was a problem hiding this comment.
Does this break semver (in a way that matters) for any consumers?
If so, would be worth keeping as Pluralize rather than version bumping the lib?
There was a problem hiding this comment.
Well, version bumping will be needed since we plan to extend {% call %} blocks. So it's fine.
There was a problem hiding this comment.
The type was unreachable before, so its name did not matter.
| fn as_filtered(&self) -> Result<Option<Self::Filtered<'_>>, Self::Error>; | ||
| } | ||
|
|
||
| const _: () = { |
There was a problem hiding this comment.
This isn't a syntax I've seen before, what's the rationale behind it?
There was a problem hiding this comment.
To prevent impacting the upper scope.
There was a problem hiding this comment.
In here I mostly use it like a #region. Every DefaultFilterable is in this scope and one click hides everything.
There was a problem hiding this comment.
Got it. I know that often there's a tendency for larger module sizes in some Rust projects. This seems to be a symptom of that. I tend to address the same core problem (strongly related code that isn't in the outer scope) by using smaller narrower module (files) and liberal re-exports instead. This keeps the unit tests fairly close to the code it's testing, makes it easy to navigate and find code, etc. Obv. this is subjective, but maybe something to think about in future.
So a soft suggestion to extract this to filters/default.rs or something.
| </li><li> | ||
|
|
||
| ```jinja | ||
| {{ variable_or_expression | default(default_value, true) }} |
There was a problem hiding this comment.
I'd still prefer explicit over implicit a bit. This is based on an assumption that the direction of askama is a jinja-like template library with natural rust idioms rather than a jinja compatible template library that adds rust conveniences. I'm not sure whether that assumption is correct however (intuitively I'd lean towards the former as being more useful)
As a single data point. Having not seen the jinja syntax for the default filter prior, even with the parameter named boolean in the CoPilot suggestion rather than the bare true, I still had no idea from reading it what that parameter meant or controlled seeing it for the first time. This suggests to me that it is poorly named in jinja, and something that we could make better pretty easily with good naming here.
|
So, what do we want to do with the argument "boolean"? It's the name and semantics Jinja uses. The question is: How compatible do we want to be? If we don't care to be 100% compatible (and we don't), then yes, two filters would be the better option:
Fun fact minijinja does not implement |
|
Or, what about both options?
The filter |
|
Having two filters for the same thing sounds good to me. 👍 Just please improve the argument name. We can add an error for this specific case to tell that for better naming, What do you think @joshka? |
|
Supporting jinja syntax probably makes it easier to bring templates from elsewhere (and allows LLMs to fall into the pit of success by suggesting code which works even if it's sub optimal). So while I dislike the syntax, if there's a better syntax available like suggested above, then I've no complaints about also providing compatible syntax like this. I'd even go so far as to suggest that the parameter name is fine in that case (alternatively, if there's some future ability to alias it to give it an additional more descriptive name then that would work too) |
|default|default, |assigned_or and |defined_or
eef03df to
826194d
Compare
|
I added I don't think that it would be a good idea to implement
|
|
The other names are good. I like the idea of making it possible to do something useful if someone tries jinja syntax. That makes a lot of sense as it makes it easy to fall into a pit of success. But I also like the idea of making this a compile error, but wonder if it might be possible to cause a deprecation warning to be emitted for |
|
I could not think of a use case. In python, the Right now, a proc-macro can only emit fatal errors, no warnings. There are open RFCs to add #[deprecated(note = "use `|defined_or` or `|assigned_or` instead of `|default`")]
fn dont_use_default() {}Most warnings that come from proc-macros get suppressed, though. I would need to test if deprecation message are one of them. |
c307767 to
6f5a0f6
Compare
|
We can't assign a span to the warning (→ #420). Without the span, such a warning is kinda useless IMHO. I think the current implementation is fine as it is. :) We can always add a warning in a subsequent PR. @GuillaumeGomez, @joshka, what do you think? Good enough to be merged? |
|
Output looks ok to me. |
joshka
left a comment
There was a problem hiding this comment.
Some extra rview notes having gone through this in a bit more detail.
| fn as_filtered(&self) -> Result<Option<Self::Filtered<'_>>, Self::Error>; | ||
| } | ||
|
|
||
| const _: () = { |
There was a problem hiding this comment.
Got it. I know that often there's a tendency for larger module sizes in some Rust projects. This seems to be a symptom of that. I tend to address the same core problem (strongly related code that isn't in the outer scope) by using smaller narrower module (files) and liberal re-exports instead. This keeps the unit tests fairly close to the code it's testing, makes it easy to navigate and find code, etc. Obv. this is subjective, but maybe something to think about in future.
So a soft suggestion to extract this to filters/default.rs or something.
| label = "`{Self}` is not `|assigned_or` filterable", | ||
| message = "`{Self}` is not `|assigned_or` filterable" | ||
| )] | ||
| pub trait DefaultFilterable { |
There was a problem hiding this comment.
It could be worth adding a unit tests for the various implementations of this that captures each implementation. E.g.:
assert_eq!(0_u8.as_filtered(), Ok(None));
assert_eq!(1_u8.as_filtered(), Ok(Some(1));|
|
||
| ```jinja | ||
| {% let greeting = Some("Hello") %} | ||
| {{ greeting.as_ref() | default("Hi", true) }} |
There was a problem hiding this comment.
should be
| {{ greeting.as_ref() | default("Hi", true) }} | |
| {{ greeting.as_ref() | assigned_or("Hi") }} |
| {{ variable_or_expression | assigned_or(fallback) }} | ||
| {{ variable_or_expression | assigned_or(fallback) }} |
There was a problem hiding this comment.
duplicate line - needs to be fixed or state what's going on here.
| This filter works like the Jinja filter of the same name. | ||
| If the second argument is not an boolean `true`, then the filter behaves like [`|defined_or`][#defined_or]. | ||
| If it is supplied and `true`, then the filter behaves like [`|assigned_or`][#assigned_or]. |
There was a problem hiding this comment.
This should maybe describe how this works without having to link to the jinja / the other filters.
| If it is supplied and `true`, then the filter behaves like [`|assigned_or`][#assigned_or]. | ||
|
|
||
| **This filter exists for compatibility with Jinja. | ||
| You should use [`|defined_or`][#defined_or] and [`|assigned_or`][#assigned_or] directly instead.** |
There was a problem hiding this comment.
| You should use [`|defined_or`][#defined_or] and [`|assigned_or`][#assigned_or] directly instead.** | |
| Askama provides [`|defined_or`][#defined_or] and [`|assigned_or`][#assigned_or] which both better express the intention and should generally usually be used instead of this filter.** |
| "var | default }}" | ||
| --> tests/ui/default.rs:10:35 | ||
| | | ||
| 10 | #[template(ext = "html", source = "{{ var | default }}")] |
There was a problem hiding this comment.
This actually suggests a bit of a jinja / rust impedance mismatch here. In Rust, we have a few different operations like map_or_default and unwrap_or_default. Intuitively, var | default when the type of var implements the Default trait seems like it would be obvious that it should return default. I know that a user could call .unwrap_or_default() or whatever there, but I'm curious whether it might be worth extending this to allow the obvious intuitive thing there. (I'm not fully certain of this, either way, WDYT?)
There was a problem hiding this comment.
The default filter is not a great fit for rust, but sadly, I'm not sure if we can go around this issue in an elegant way without making it harder for both jinja and rust users to be confused. At least with this, jinja templates work with askama...
|
@joshka, thank you for your suggestions and proofreading! I will have a look tomorrow. |
Also, `enum Pluralize<S, P>` is renamed into `enum Either<L, R>` and exported.
|
Took me a little longer than a day to look into it. 😳 |
|
|
||
| #[test] | ||
| fn test_default_with_str() { | ||
| // In this test a default value should be returned for an empty string. |
There was a problem hiding this comment.
Comments explaining what the test checks is very appreciated, thanks! :)
| 40 | #[template(ext = "html", source = "{{ var | defined_or }}")] | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
|
||
| error[E0277]: `Mutex<u32>` is not `|assigned_or` filterable |
There was a problem hiding this comment.
I didn't know we could do that in rust, it looks super nice! (and will look even better once I add the spans to the generated TokenStream :3)
There was a problem hiding this comment.
The feature is already stable for over a year: https://releases.rs/docs/1.78.0/, but I am not sure if I ever saw it in the wild. I found it in the standard library and simply tested if library developers can use it, too. :)
There was a problem hiding this comment.
A lot of features are, but if I don't use them, I simply forget about them. ^^'
|
Looks all good to me, thanks! |
|
Thank you both so much for your ideas, suggestions and reviews! :) |
Also,
enum Pluralize<S, P>is renamed intoenum Either<L, R>and exported.Cc @joshka.
Resolves #424.