rainier_database/model.rs
1//! The [`Model`] contract and its lifecycle **hooks**.
2//!
3//! A model is a Rainier ORM [`Entity`] with the extra facts the framework needs:
4//! a display name for error messages, and the column route-model binding looks
5//! up by. Declaring one is a single line, because everything else is derived:
6//!
7//! ```
8//! use rainier_orm::Entity;
9//! use rainier_database::Model;
10//!
11//! #[derive(Entity, Clone)]
12//! #[orm(table = "posts")]
13//! struct Post {
14//! #[orm(pk, auto_increment)]
15//! id: u64,
16//! #[orm(unique)]
17//! slug: String,
18//! title: String,
19//! }
20//!
21//! impl Model for Post {
22//! // Bind `/posts/{post}` by slug rather than by id.
23//! fn route_key_name() -> &'static str { "slug" }
24//! }
25//! ```
26//!
27//! ## Lifecycle hooks
28//!
29//! Every repository write dispatches events through
30//! [`rainier_events`](rainier_events::Dispatcher), which is how the model
31//! lifecycle hooks — creating, created, saved, deleted — are spelled here. Each is a
32//! distinct type, so a listener registers for exactly the moment it cares
33//! about rather than matching on a discriminant:
34//!
35//! ```
36//! # use rainier_orm::Entity;
37//! # use rainier_database::{Created, Model};
38//! # use rainier_events::Dispatcher;
39//! # use std::sync::Arc;
40//! # #[derive(Entity, Clone)]
41//! # struct Post { #[orm(pk)] id: u64, title: String }
42//! # impl Model for Post {}
43//! # fn wire(events: &Dispatcher) {
44//! events.listen(|event: Arc<Created<Post>>| async move {
45//! println!("created post {}", event.model.title);
46//! Ok(())
47//! });
48//! # }
49//! ```
50//!
51//! ### What a hook can and cannot do
52//!
53//! A `-ing` hook (`Creating`, `Updating`, `Deleting`) runs **before** the write
54//! and can **veto** it by returning `Err` — the repository propagates the error
55//! and never touches the database. It cannot *modify* the model: listeners
56//! receive a shared `Arc`, and letting several listeners mutate one row in an
57//! unspecified order would make the outcome depend on registration order.
58//! Derive values in the model's own constructor, or in the repository, where
59//! there is exactly one place to look.
60//!
61//! A `-ed` hook (`Created`, `Updated`, `Deleted`) runs **after** a successful
62//! write. Returning `Err` from one does not roll anything back; it only
63//! surfaces to the caller.
64
65use std::any::type_name;
66
67use rainier_orm::Entity;
68use rainier_support::str::class_basename;
69
70/// An entity the framework manages.
71///
72/// `Clone` is required because lifecycle hooks receive the model by value:
73/// a repository hands a copy to the event bus and keeps the original for the
74/// write. The clone is skipped entirely when nothing is listening.
75pub trait Model: Entity + Clone + Send + Sync + 'static {
76 /// The model's name, for error messages — `"Post"`.
77 fn model_name() -> &'static str {
78 class_basename(type_name::<Self>())
79 }
80
81 /// The column route-model binding looks a member up by.
82 ///
83 /// Defaults to the primary key; override to bind by slug or UUID.
84 fn route_key_name() -> &'static str {
85 Self::primary_key()
86 }
87}
88
89/// Fired before a row is inserted. Returning `Err` cancels the insert.
90#[derive(Debug, Clone)]
91pub struct Creating<M> {
92 /// The model about to be inserted.
93 pub model: M,
94}
95
96/// Fired after a row is inserted.
97#[derive(Debug, Clone)]
98pub struct Created<M> {
99 /// The inserted model, with its assigned key where the backend reported
100 /// one.
101 pub model: M,
102 /// The key the database assigned, or `0` for an app-assigned key.
103 pub id: i64,
104}
105
106/// Fired before a row is updated. Returning `Err` cancels the update.
107#[derive(Debug, Clone)]
108pub struct Updating<M> {
109 /// The model about to be written.
110 pub model: M,
111}
112
113/// Fired after a row is updated.
114#[derive(Debug, Clone)]
115pub struct Updated<M> {
116 /// The model as written.
117 pub model: M,
118 /// How many rows the update touched.
119 pub rows_affected: u64,
120}
121
122/// Fired before a row is deleted. Returning `Err` cancels the delete.
123#[derive(Debug, Clone)]
124pub struct Deleting<M> {
125 /// The primary key about to be deleted.
126 pub key: String,
127 /// The model type, for listeners registered generically.
128 pub model: std::marker::PhantomData<M>,
129}
130
131/// Fired after a row is deleted.
132#[derive(Debug, Clone)]
133pub struct Deleted<M> {
134 /// The primary key that was deleted.
135 pub key: String,
136 /// How many rows the delete removed.
137 pub rows_affected: u64,
138 /// The model type, for listeners registered generically.
139 pub model: std::marker::PhantomData<M>,
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[derive(rainier_orm::Entity, Clone, Debug)]
147 #[orm(table = "posts")]
148 struct Post {
149 #[orm(pk, auto_increment)]
150 id: u64,
151 #[orm(unique)]
152 slug: String,
153 title: String,
154 }
155
156 impl Model for Post {}
157
158 #[derive(rainier_orm::Entity, Clone, Debug)]
159 #[orm(table = "pages")]
160 struct Page {
161 #[orm(pk, auto_increment)]
162 id: u64,
163 #[orm(unique)]
164 slug: String,
165 }
166
167 impl Model for Page {
168 fn route_key_name() -> &'static str {
169 "slug"
170 }
171 }
172
173 #[test]
174 fn the_model_name_is_the_type_name_without_its_module_path() {
175 assert_eq!(Post::model_name(), "Post");
176 }
177
178 #[test]
179 fn the_route_key_defaults_to_the_primary_key() {
180 assert_eq!(Post::route_key_name(), "id");
181 assert_eq!(Post::route_key_name(), Post::primary_key());
182 }
183
184 #[test]
185 fn the_route_key_can_be_overridden() {
186 assert_eq!(Page::route_key_name(), "slug");
187 assert_eq!(Page::primary_key(), "id", "the primary key is unchanged");
188 }
189
190 #[test]
191 fn the_entity_metadata_still_comes_from_the_derive() {
192 assert_eq!(Post::table(), "posts");
193 assert_eq!(Post::columns().len(), 3);
194 }
195
196 #[test]
197 fn hook_events_carry_the_model() {
198 let post = Post { id: 1, slug: "hi".into(), title: "Hi".into() };
199 let creating = Creating { model: post.clone() };
200 assert_eq!(creating.model.title, "Hi");
201
202 let created = Created { model: post, id: 7 };
203 assert_eq!(created.id, 7);
204 }
205}