CRUD with forms
Registration form
Manual form (without model)
// src/forms.rs
use runique::prelude::*;
pub struct RegisterForm {
pub form: Forms,
}
#[async_trait]
impl RuniqueForm for RegisterForm {
impl_form_access!();
fn register_fields(form: &mut Forms) {
form.field(
&TextField::text("username")
.label("Username")
.required()
.min_length(3, "Minimum 3 characters")
.max_length(50, "Maximum 50 characters")
);
form.field(
&TextField::email("email")
.label("Email")
.required()
);
form.field(
&TextField::password("password")
.label("Password")
.required()
.min_length(8, "Minimum 8 characters")
);
}
// Business validation — called automatically by is_valid()
async fn clean(&mut self) -> Result<(), StrMap> {
let mut errors = StrMap::new();
if !self.cleaned_string("email").unwrap_or_default().contains('@') {
errors.insert("email".to_string(), "Invalid email".to_string());
}
if errors.is_empty() { Ok(()) } else { Err(errors) }
}
}
Model-based form
#[form(...)] generates the struct and impl ModelForm.
The developer writes impl RuniqueForm with impl_form_access!(model):
use runique::prelude::*;
#[form(schema = users_schema, fields = [username, email, password])]
pub struct RegisterForm;
#[async_trait]
impl RuniqueForm for RegisterForm {
impl_form_access!(model);
async fn clean(&mut self) -> Result<(), StrMap> {
let mut errors = StrMap::new();
if self.cleaned_string("username").unwrap_or_default().len() < 3 {
errors.insert("username".to_string(), "Minimum 3 characters".to_string());
}
if !self.cleaned_string("email").unwrap_or_default().contains('@') {
errors.insert("email".to_string(), "Invalid email".to_string());
}
if self.cleaned_string("password").unwrap_or_default().len() < 10 {
errors.insert("password".to_string(), "Minimum 10 characters".to_string());
}
if errors.is_empty() { Ok(()) } else { Err(errors) }
}
}
#[async_trait]is required only when overridingcleanorclean_field. Without async override,impl RuniqueForm { impl_form_access!(model); }is enough.
Registration handler
pub async fn signup(mut request: Request) -> AppResult<Response> {
let form: RegisterForm = request.form();
let template = "signup_form.html";
let mut validated = match ValidationForm::try_new(form, &request).await {
Ok(validated) => validated,
Err(form) => {
context_update!(request => {
"title" => "Validation Error",
"signup_form" => &form,
"messages" => flash_now!(error => "Please fix the errors"),
});
return request.render(template);
}
};
let user = validated.save(&request.engine.db).await.map_err(|err| {
validated.database_error(&err);
AppError::from(err)
})?;
success!(request.notices => format!("Welcome {}!", user.username));
Ok(Redirect::to("/").into_response())
}
Registration template
{% extends "base.html" %}
{% block content %}
<h1>{{ title }}</h1>
{% messages %}
<form method="post" action='{% link "signup" %}'>
{% form.signup_form %}
<button type="submit">Sign up</button>
</form>
{% endblock %}
Search and display an entity
Search form
pub struct UsernameForm {
pub form: Forms,
}
impl RuniqueForm for UsernameForm {
fn register_fields(form: &mut Forms) {
form.field(
&TextField::text("username")
.label("Username")
.required()
.placeholder("Search a user")
);
}
impl_form_access!();
// This form only serves the GET search — never validate on POST.
fn allow_post(&self, _request: &Request) -> bool {
false
}
// Explicit opt-in: a read-only search form is the one case that should
// auto-validate on GET (`allow_get` defaults to `false`).
fn allow_get(&self, _request: &Request) -> bool {
self.is_submitted()
}
}
Search handler
pub async fn info_user(mut request: Request) -> AppResult<Response> {
let form: UsernameForm = request.form();
let template = "profile/view_user.html";
match ValidationForm::try_new(form, &request).await {
Ok(validated) => {
let form = validated.into_form();
let username = form.cleaned_string("username").unwrap_or_default();
let db = request.engine.db.clone();
let user_opt = UserEntity::find()
.filter(user::Column::Username.eq(&username))
.one(&*db)
.await
.unwrap_or(None);
match user_opt {
Some(user) => {
context_update!(request => {
"title" => "User view",
"found_user" => &user, // ⚠️ DO NOT name it "user" → collision with the form
"user" => &form,
"messages" => flash_now!(success => "User found!"),
});
}
None => {
context_update!(request => {
"title" => "User view",
"user" => &form,
"messages" => flash_now!(warning => "User not found"),
});
}
}
request.render(template)
}
Err(form) => {
context_update!(request => { "title" => "Search a user", "user" => &form });
request.render(template)
}
}
}