Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Rust verbatim or use Jinja-esque syntax #95

Open
anna-is-cute opened this issue Jun 22, 2018 · 5 comments
Open

Use Rust verbatim or use Jinja-esque syntax #95

anna-is-cute opened this issue Jun 22, 2018 · 5 comments

Comments

@anna-is-cute
Copy link

anna-is-cute commented Jun 22, 2018

It appears that most of what is in an if block gets translated straight into the generated Rust code, but askama fails to parse much Rust syntax. In my opinion, askama should either accept Rust code verbatim in blocks or warp it to work like Jinja.

Currently, I can't figure out a better way to do this. Option support in general is difficult, even with match, and [ and ] can't be used for Index. Similarly, comparison can't be done a la == Some("string literal"), but == "string literal".into() works.

{% if form.is_some() && form.as_ref().unwrap().index("name").as_str().is_some() %}
value="{{ form.as_ref().unwrap().index("name").as_str().unwrap() }}"
{% endif %}

To make things worse, I can't use .map(...) because pipes (|) cause the template to fail to parse.

If handled like Jinja, though, it turns into this.

{% if form.name %}
value="{{ form.name }}"
{% endif %}

Another point where I was confused about how to do something in askama due to not knowing what is and isn't allowed in terms of syntax:

{{ some_string | truncate(len=7) }}

No truncate filter, so what about taking a substring?

{{ &some_string[..7] }}

No, that doesn't parse.

{{ some_string.index(..7) }}

That also doesn't parse. Presumably using .. and ..= notation will not parse, so Ranges cannot be created that way. I assume that since you can't do Some(x), you can't create a Range struct, either.

Basically, letting the user use methods on variables is nice, but it's unclear how exactly they can be used. Rust types being used directly in the template is really useful for some things, but not having Jinja-esque patterns to deal with them makes code unreadable, confusing, and unwieldy (see the first example above).

I'm never sure if I'm supposed to use code that looks like Jinja or use code that looks like Rust.


I realise that it's extremely difficult to have some of these things work, but I'm just pointing out a huge source of confusion and frustration for me when I'm trying to use askama. I still have no idea how I'm supposed to substring besides making my own trait and implementing it on str.

@djc
Copy link
Owner

djc commented Jun 23, 2018

First off, a meta-comment: your bug reports so far often employ a tone that rubs me the wrong way, as in "extremely useful", "extremely useful", "gigantic blocker" (all in the initial post in #85), and in this case "extremely difficult", "huge source of confusion and frustration". I understand that you're frustrated, but implicitly blaming that on me or the tool I created seems neither fair nor productive. Maybe you should write the bug report after a bit of a cooling down?

Second, if you want to suggest solutions, you're going to have to work a bit harder to understand the constraints in which Askama is operating, which make some of the things you suggest just impossible. With a static type system and very limited type information available to it, the code generator can only do so much, so I carefully try to balance Askama between being expressive and not take on the complexity of trying to reproduce the full complexity of Rust in templates. That's not what this templating tool is designed for, anyway.

Third, solutions that work today that you could employ today that seem fairly usable to me: if you have something like an Option<HashMap<String, String>>, you could handle it as such:

{% match form %}
  {% when Some with (form) %}
    value={{ form.get("name").unwrap() }}
  {% else %}
{% endmatch %}

This seems like a reasonably concise solution, though definitely not as concise as in Jinja (but I don't think that level of conciseness is technically possible in a statically typed language like Rust).

For some_string|truncate(len=7), while it's true that Askama doesn't have a truncate filter today, it does support user defined filters, so that you could always define a truncate filter yourself. In fact, user-defined filters could also provide a solution for complicated unwrapping code like the one in your first example, to try to move some of the complexity back into actual Rust code. In general, the Askama template language is not designed for computation, it's mostly designed to concate strings with some finishing touches on how those strings get represented.

All that said, I think there are a few things that would be straightforward to improve (and thanks for the guide to all the things that you tried, which informed this list):

  • Add support for indexing (var[expr])
  • Add support for range literals (foo..bar with optional start and end and inclusivity)
  • Add a built-in truncate filter

djc added a commit that referenced this issue Jun 23, 2018
@anna-is-cute
Copy link
Author

Thanks for your reply.

In response to your meta comment, I apologise if these things come off in a negative way. I'm not sure why calling things useful or blockers for use rub you the wrong way, but I have no ill intent, and I'm definitely not trying to blame you or askama for anything. None of the issues I've written have been written with anger. The "extremely difficult" you mention is actually sympathetic, since I understand that many of these things may not be possible or may be... extremely difficult to do. The disconnect between expectations and reality when using askama, however, is a source of frustration and confusion for me, and I think that any software author should want to know what is causing their users frustration.

The point is not that you should make askama exactly equivalent to Jinja. That would be nice, but I think that it's not feasible when dealing with Rust and its type system so directly. The point I meant to make, whether or not I did so successfully, is that it's very confusing to use askama. I do not see any documentation for what is and isn't expected in code blocks, and sometimes the necessary workarounds seem overly complex.

The actual necessary code to do what the form example did without unwrapping, since each member is optional, would be as follows.

{% match form %}
  {% when Some with (form) %}
    {% match form.get("name") %}
      {% when Some with (name) %}
        value="{{ name }}"
      {% else %}
    {% endmatch %}
  {% else %}
{% endmatch %}

I don't know the nitty-gritty details of how askama works, but I have a general idea. Perhaps there is some way to do the following, where Try is leveraged.

{% try %}
{{ form?.name? }}
{% endtry %}
fn generated_name() -> Option<String> {
  form?.name?
}

if let Some(generated_name) = generated_name() {
  // do writing
}

This would need to be adapted for Result as well as Option, if this is even feasible. Again, I expect you might have better ideas as to how to handle this, if you even think it needs addressing.

I only suggested the "solutions" of using Rust verbatim or using a Jinja-esque system because it's currently unclear which you are expected to use when templating with askama.

Don't get me wrong. I really enjoy askama and its idea, and I'm really excited to continue using it.

djc added a commit that referenced this issue Jun 23, 2018
@djc
Copy link
Owner

djc commented Jun 23, 2018

To me, the way you use "blocker" or "useful" in combination with these superlatives exaggerates the importance of your particular use case, when, according to download statistics and lack of bug reports so far, quite a few people have been able to use Askama just fine without these features.

As for Try, remember that Askama aims to be usable on stable Rust. On the other hand, the idea of using a ? operator is interesting. Let me think about that some more.

@djc
Copy link
Owner

djc commented Jun 25, 2018

The problem with using ? this way is the need to name the unwrapped value. This means that an expression like form?.get("name")? could work, but it you need to get some value="" around it you'd still need to revert to the more verbose way of writing it. I don't think there's an obvious way of assigning the value in a way that would be better than finding some more generic syntax to do if let-type things. Also, you may in the end be correct that creating a separate syntax for match was not the right choice, and we should support full Rust match syntax instead. However, that would make the implementation significantly more complex, so that I'm not sure it's worth it.

I've played around with the expression form, but it can only be supported in a subset of cases (for example, I don't think a? && b has no obvious meaning. So far, integrating it into the parser correctly also proved hard.

djc added a commit that referenced this issue Jun 25, 2018
@anna-is-cute
Copy link
Author

Being able to use ? anywhere would be great, but I think a potential try block that enables the syntax could be a good stepping stone.

When I wrote about the potential try block, I was thinking it might need to be given a type.

{% try Option %}
  {{ a?.b }}
{% endtry %}

Where a is some Option<T> where T has a field b, and none of the types (besides Option, which is given) should have any consequence in the generated code.

At least in this case, it makes the nested match blocks unnecessary, even though it's still a little more verbose than what would be ideal.

To use your example, what I was thinking was the following.

{% try Option %}
  A
  {{ form?.get("name")? }}
  B
{% endtry %}
// let's just ignore whitespace in this example
// struct Context { form: Option<HashMap<String, String>> }

// try block uses Option, so we use Option and Some
// if it had specified Result, though, things get trickier when specifying the return result
let try_block = || -> Option<()> {
  write("A");
  let value = self.form?.get("name")?;
  write("{}", value);
  write("B");
  Some(())
};
try_block();

Alternatively, each expression could be wrapped in a closure and then looked at via if let, but I think that's harder to do when generating the code, and it doesn't stop execution of later code if ? returns None.

The closure would probably need to be given self, and I'm not 100% sure if it's possible to handle these blocks like this.

My thinking is that you were looking at doing a match or if let on each variable that ? was used on? I didn't think about doing it that way, and that sounds like a pretty good way to do it, actually.

The problem with using ? this way is the need to name the unwrapped value. This means that an expression like form?.get("name")? could work, but it you need to get some value="" around it you'd still need to revert to the more verbose way of writing it.

What specifically poses a problem with naming the unwrapped value? In the generated code, I see let askama_value = ... or something similar (can't remember the name) for variables. What prevents that in this case?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants