Ah, sorry - looks like I braindumped too late at night. Lemme back up and break down these two last points.
For context, in my application, I define my handlers as the following:
pub async fn my_handler(mut request: RequestData<T>) -> crate::Result<Response>;
T
is a stand-in here for whatever custom type a handler wants to store in the request data, and Response
is a mostly standard axum::Response
. RequestData<T>
itself is a custom extractor that does the work of globbing the litany of other extractors into one type (e.g AuthSession, Messages, etc), which alleviates quite a bit of tedious code when using Askama: any Askama template I render now just accepts a request
property, rather than needing to always wire up various properties. [1]
You mean such that messages = messages.info("Hello, world!"); doesn't work for the use case? I think an example of what you mean here would be helpful.
Given the above RequestData<()>
type, and the current APIs on Messages
, I can't do the following:
request.messages.error("Oh no, an error");
The issue here is that Messages
is a builder pattern and returns itself, but Messages
itself isn't Copy
- and as a result it becomes trickier to work with in contexts where I want something higher-level to just manage it. I know I need Messages on every view for a global layout area.
This can be worked around by doing something with Clone
, but needing to Clone
internally just to push to a type that I have mutable ownership of feels weird. I understand not wanting to break a public API, but I'd like to respectfully suggest that the builder interface isn't an appropriate pattern here - prior to Rust I did most of my web dev career with Django, and I never needed to build a chain of flash messages in one spot in the codebase, they were almost always injected from different validation spots in the codebase.
Maybe methods like the following could be added?
impl Messages {
pub fn add_error(&mut self, message: impl Into<String>) { ... }
}
messages.add_error("Oh no, an error");
The second issue I noted is the separation of pending_messages
and messages
, which I surmise is owing to needing to commit pending_messages
at the end of the request lifecycle. This makes sense from a framework perspective, but the following scenario breaks down with it - let's consider a very simplified "Reset Password" form handler:
pub async fn update_password(
mut request: RequestData<T>,
Form(form): Form<MyForm>
) -> crate::Result<Response> {
if let Err(e) = my_validation_method(&form) {
// This flash message will not render in the template, because it's added to
// pending_messages and not moved into messages (which iter() pulls from)
// until the request lifecycle is finished. The render to string technically happens
// here though.
request.messages.error("Internal error flash message");
return render(MyTemplate {
request,
form
});
}
// This flash message *will* render, because the redirect goes through the full request lifecycle
// and rehydrates the session after saving
request.messages.info("Great success");
redirect(StatusCode::SEE_OTHER, "/my/url/")
}
In the case of this error, I want to re-render the form (with anything they modified on the form so far) and display an error to the user. It's unfortunately not possible as messages.iter()
only pulls from messages
, but this .error
pushes into pending_messages
.
To work around this, I currently just use a local fork that has the following method added:
/// Drains the *entire* messages stack.
pub fn drain(&self) -> Vec<Message> {
let mut data = self.data.lock();
let mut messages = vec![];
for queue in [
std::mem::take(&mut data.messages),
std::mem::take(&mut data.pending_messages)
] {
for message in queue.into_iter() {
messages.push(message);
}
}
if !self.is_modified() {
self.is_modified.store(true, atomic::Ordering::Release);
}
messages
}
Then in my template, I can now just do:
<section id="messages">
{% for message in request.messages.drain().into_iter() %}
<div class="msg msg-{{ message.level }}">{{ message }}</div>
{% endfor %}
</section>
Hopefully that breaks down better what I was spewing late at night, but let me know if I can illustrate further - they're not full code samples but I think they should get the gist across. Also, I just want to be clear - I think your work is great and I 100% don't want to come across as trashing it or anything, these are mostly aiming to be feedback that might help a wider set of use-cases. :)
[1] I'll refrain from ranting about why the extractor pattern that Rust frameworks tend to use isn't always great on Support extracting messages in one pass?
a week ago