Link with forms & technical considerations
Link with forms via #[form(...)]
The #[form(...)] attribute macro expects:
schema = function_path(required)fields = [..](optional)exclude = [..](optional)model = Entity(optional) — links the form to a SeaORM entity
It generates:
- a struct containing
form: Forms impl ModelForm(schema(),fields(),exclude())- if
modelis present:impl FormEntity+pub const objects
The developer then writes impl RuniqueForm with impl_form_access!(model):
use runique::prelude::*;
#[form(schema = user_schema, fields = [username, email])]
pub struct UserForm;
impl RuniqueForm for UserForm {
impl_form_access!(model);
}
With model — ORM access from the form
Adding model = Entity makes the form a direct entry point for ORM queries, without going through the entity type.
#[form(schema = user_schema, model = users::Entity)]
pub struct UserForm;
#[form(schema = blog_schema, model = blog::Entity)]
pub struct BlogForm;
#[form(schema = document_schema, model = document::Entity)]
pub struct DocumentForm;
This automatically generates:
impl FormEntity for UserForm {
type Entity = users::Entity;
}
impl UserForm {
pub const objects: Objects<users::Entity> = Objects::new();
}
Direct ORM access via the form:
// All records
let users = UserForm::objects.all().all(&db).await?;
// With filter
let user = UserForm::objects
.filter(users::Column::Email.eq("alice@example.com"))
.first(&db)
.await?;
// Via the search! macro
let results = search!(@UserForm => Username = "alice").all(&db).await?;
let adults = search!(@UserForm => Age >= 18).all(&db).await?;
// All search! operators are supported
let results = search!(@BlogForm =>
Status = ("published" | "featured"),
AuthorId = author_id,
)
.order_by_desc(blog::Column::CreatedAt)
.limit(10)
.all(&db)
.await?;
modelis compatible withfieldsandexclude— they can be freely combined.
#[form(schema = user_schema, fields = [username, email], model = users::Entity)]
pub struct UserForm;
With business validation (clean)
Override clean directly in impl RuniqueForm — just like Django.
#[async_trait] is only required when overriding an async method:
#[form(schema = user_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.get_string("username").len() < 3 {
errors.insert("username".to_string(), "Minimum 3 characters".to_string());
}
if !self.get_string("email").contains('@') {
errors.insert("email".to_string(), "Invalid email".to_string());
}
if errors.is_empty() { Ok(()) } else { Err(errors) }
}
}
is_valid()automatically callscleanafter structural validation. Returned errors are attached to fields and displayed inline in the template.
Technical considerations
Advantages
- Single model/schema contract, centralized
- Coherent generation of migrations + forms
- Reduced duplication of field definitions
cleanis the official trait override — uniform between manual and model-based forms
Points of attention
- Strict DSL: a syntax error causes a macro build error
- Misaligned
fields/excludewith the schema can cause generation or runtime errors #[async_trait]required onimpl RuniqueFormonly when overridingcleanorclean_field
Known limitation — field override not yet supported
Overriding an individual field auto-generated by
#[form(...)]ormodel!is not yet supported.
It is currently not possible to customize a single field (e.g. add .max_size_mb(5) or change the label) without rewriting the entire register_fields by hand, which defeats the purpose of the macro.
Current workaround: write a fully manual form and declare all fields explicitly.
// ❌ Not yet possible
#[form(schema = article_schema)]
pub struct ArticleForm;
impl RuniqueForm for ArticleForm {
impl_form_access!(model);
// override just the image field → impossible without rewriting everything
}
// ✅ Workaround: manual form
impl RuniqueForm for ArticleForm {
impl_form_access!();
fn register_fields(form: &mut Forms) {
form.field(&TextField::text("title").label("Title").required());
form.field(
&FileField::image("image")
.upload_to("media/articles")
.max_size_mb(5),
);
}
}
This limitation will be addressed in v2.0 with the refactoring of the field system into widgets, which will allow declaring and overriding any field directly from the model.