rainier_framework/lib.rs
1//! # Rainier
2//!
3//! A Laravel-shaped MVC framework for Rust, built on the
4//! [Rainier ORM](https://github.com/safewords/rainier-framework/tree/main/crates/rainier-orm) DBAL.
5//!
6//! Rainier takes Laravel's *structures* — a service container, service
7//! providers, a router with named routes and middleware groups, form requests,
8//! guards, jobs, mailables, facades, events — and gives each one a Rust shape
9//! rather than a transliterated PHP one. Where the two languages disagree, the
10//! disagreement is resolved in Rust's favour and the reason is written down in
11//! the module that made the call.
12//!
13//! ```no_run
14//! use rainier_framework::prelude::*;
15//!
16//! async fn index() -> &'static str {
17//! "Hello from Rainier"
18//! }
19//!
20//! #[tokio::main]
21//! async fn main() -> Result<()> {
22//! let app = Rainier::new(".")
23//! .with_routes(|router| {
24//! router.get("/", index).name("home");
25//! })
26//! .boot()
27//! .await?;
28//!
29//! rainier_framework::console("rainier").run_from_env(&app).await;
30//! Ok(())
31//! }
32//! ```
33//!
34//! ## The crates
35//!
36//! Each component is its own crate, and depends only on what it genuinely
37//! needs — so an application that wants the queue but not HTTP pays for
38//! neither the router nor hyper.
39//!
40//! | Crate | What it owns |
41//! |---|---|
42//! | [`support`] | the error type, futures, type-maps, string inflection |
43//! | [`rainier_orm`] | the DBAL: entities, queries, DDL, sharding, the `Executor` port |
44//! | [`container`] | the IoC container, service providers, lifecycle hooks, facades |
45//! | [`config`] | the config repository and `.env` |
46//! | [`events`] | the event dispatcher — the hook bus |
47//! | [`http`] | requests, responses, cookies, uploads, extractors |
48//! | [`middleware`] | the `handle(request, next)` pipeline and the built-in set |
49//! | [`routing`] | route declaration, groups, resources, URL generation |
50//! | [`validation`] | rules, the validator, and **request contracts** |
51//! | [`view`] | the Blade-style template engine |
52//! | [`database`] | Rainier ORM integration, models, repositories |
53//! | [`auth`] | guards, user providers, hashing, gates |
54//! | [`queue`] | jobs, queue drivers, the worker |
55//! | [`mail`] | mailables, the mailer, transports |
56//! | [`server`] | the HTTP kernel and the hyper server |
57//! | [`console`] | the artisan-style console |
58//! | [`cache`] | the cache port and its drivers |
59//! | [`crypt`] | encryption and message signing |
60//! | [`drivers`] | shared Redis, Memcached and AWS transports |
61//! | [`filesystem`] | file storage: local, memory, S3/R2 |
62//! | [`notify`] | notifications: a message to a recipient, over their channels |
63//! | [`scheduler`] | cron expressions, the schedule, and its atomic locks |
64//! | [`session`] | the session bag, its store, and the middleware |
65//!
66//! ## Where Rainier departs from Laravel
67//!
68//! - **Middleware is named, not typed, at the route.** `.middleware("auth")`
69//! keeps `routing` from depending on `auth` — see [`middleware::registry`].
70//! - **The auth manager is generic over the user model** rather than erasing
71//! it behind `dyn Authenticatable`, because an application wants its own
72//! type back. See [`auth`].
73//! - **Model hooks can veto but not mutate.** Several listeners mutating one
74//! row in registration order would make the outcome depend on wiring; see
75//! [`database::model`].
76//! - **Request bodies are buffered.** That is what lets `request.input(..)` be
77//! synchronous, as `$request->input()` is; see [`http::body`].
78//! - **Configuration is typed at both ends.** `config('cache.driver')` has a
79//! magic string for the path and another for the value. Rainier has
80//! [`keys::CACHE_DRIVER`] and a [`cache::CacheDriver`] enum, so a misspelled
81//! path does not compile and a misspelled driver fails the boot rather than
82//! silently caching in-process. See [`keys`].
83//!
84//! ## The Rainier ORM `Send` story
85//!
86//! Rainier ORM used to hold a `sea_query` statement across its awaits, which
87//! made every `repo::` future `!Send` — unusable inside a handler the server
88//! will `tokio::spawn`, since `sea_query` statements hold `Rc`. That is fixed
89//! upstream: statements are built and dropped in a scope that ends before the
90//! await, and `Query`'s terminals consume `self` outside their `async` block
91//! (an `async fn` captures every argument whether or not the body moves it
92//! out).
93//!
94//! So `repo::` and the query builder work directly against a
95//! [`database::Database`]. Rainier still renders its repository SQL
96//! synchronously through [`database::statement`] — now a design choice that
97//! keeps shard routing and dialect rendering explicit at the framework seam,
98//! rather than a workaround.
99//!
100//! One exception remains: `rainier_orm::migrate::Migrator::run` boxes its steps
101//! behind `dyn`, so its future cannot be `Send` on stable.
102//! [`database::Migrator`] is the `Send` alternative — it renders DDL
103//! synchronously and executes plain strings, and its [`Migration`] contract
104//! makes every step declare how to undo itself.
105//!
106//! [`Migration`]: database::Migration
107
108#![deny(missing_docs)]
109#![deny(rustdoc::broken_intra_doc_links)]
110
111pub mod binding;
112pub mod bootstrap;
113pub mod broadcasting;
114pub mod commands;
115pub mod facades;
116pub mod groups;
117pub mod keys;
118pub mod notifications;
119pub mod observability;
120pub mod queued_listeners;
121pub mod scheduling;
122pub mod testing;
123
124pub use bootstrap::Rainier;
125pub use commands::{
126 console, KeyGenerateCommand, MigrateCommand, MigrateRollbackCommand, QueueWorkCommand,
127 RouteListCommand, ServeCommand,
128};
129pub use facades::Views;
130pub use queued_listeners::{DispatcherExt, FromEvent};
131pub use scheduling::{
132 CommandTask, JobTask, ScheduleExt, ScheduleListCommand, ScheduleRunCommand, ScheduleWorkCommand,
133};
134pub use testing::{TestApp, TestResponse};
135
136// --- the components, re-exported under short names -------------------------
137
138pub use rainier_auth as auth;
139pub use rainier_broadcast as broadcast;
140pub use rainier_cache as cache;
141pub use rainier_config as config;
142pub use rainier_console as console_kernel;
143pub use rainier_container as container;
144pub use rainier_crypt as crypt;
145pub use rainier_database as database;
146pub use rainier_drivers as drivers;
147pub use rainier_events as events;
148pub use rainier_filesystem as filesystem;
149pub use rainier_http as http;
150pub use rainier_mail as mail;
151pub use rainier_metrics as metrics;
152pub use rainier_middleware as middleware;
153pub use rainier_notify as notify;
154pub use rainier_openapi as openapi;
155pub use rainier_queue as queue;
156pub use rainier_routing as routing;
157pub use rainier_scheduler as scheduler;
158pub use rainier_server as server;
159pub use rainier_session as session;
160pub use rainier_support as support;
161pub use rainier_telemetry as telemetry;
162pub use rainier_validation as validation;
163pub use rainier_view as view;
164pub use rainier_websocket as websocket;
165
166/// The ORM, so an application depends on one Rainier ORM version.
167pub use rainier_orm;
168
169// The macros are exported at the crate root by `#[macro_export]`, so they are
170// already reachable as `rainier_framework::facade!` and `rainier_framework::bind_executor!`.
171pub use rainier_container::{facade, Application, Facade, ServiceProvider};
172pub use rainier_database::bind_executor;
173
174/// Everything an application file usually wants in scope.
175///
176/// ```
177/// use rainier_framework::prelude::*;
178/// ```
179pub mod prelude {
180 pub use crate::bootstrap::Rainier;
181 pub use crate::facades::{
182 Broadcast, Cache, Config, Crypt, Event, Mail, Middleware, Notify, Queue, Session, Storage,
183 Url, View, Views, DB,
184 };
185
186 pub use rainier_container::{Application, Container, Facade, ServiceProvider};
187 pub use rainier_support::{build_info, BuildInfo, Context as _, Error, ErrorKind, Result};
188
189 // Typed configuration: a `Key<T>` for the path, a `Setting` for the value.
190 // The macros live at their crates' roots, so they are reachable as
191 // `rainier_framework::config::config_keys!` without being imported.
192 pub use rainier_config::{AppEnv, Key as ConfigurationKey, Setting as _};
193 pub use rainier_telemetry::LogFormat;
194
195 pub use crate::binding::{Bound, BoundAs};
196 pub use rainier_http::extract::{Bearer, Form, Json, Path, Query, RawBody};
197 pub use rainier_http::{
198 Cookie, FromRequest, Html, IntoResponse, Method, Redirect, Request, Response, StatusCode,
199 };
200
201 pub use rainier_cache::{Cache as CacheContract, CacheDriver, CacheExt as _, CacheManager};
202 pub use rainier_crypt::{EncrypterExt as _, Encryption, SignerExt as _};
203 pub use rainier_filesystem::{
204 Filesystem, FilesystemDriver, FilesystemExt as _, LocalFilesystem,
205 Storage as StorageManager,
206 };
207 pub use rainier_middleware::{
208 Compress, MethodOverride, Middleware as MiddlewareContract, Next, Timeout, TrustProxies,
209 };
210 pub use rainier_routing::{
211 GroupAttributes, ParamConstraint, Req, ResourceController, Route, Router,
212 };
213 pub use rainier_session::{
214 Session as SessionBag, SessionDriver, SessionManager, SessionRequestExt as _, StartSession,
215 };
216 pub use rainier_validation::{field, FormRequest, Rule, RuleSet, Validated, Validator};
217
218 pub use rainier_database::{
219 BelongsTo, BelongsToMany, Criteria, Database, EntityRepository, HasMany, HasOne, Model,
220 Paginated, Related, RelatedCounts, Relation, Repository,
221 };
222 pub use rainier_orm::Entity;
223
224 pub use rainier_auth::{
225 AuthManager, Authenticatable, AuthenticatedUser, Credentials, Gate, GuardExt as _,
226 };
227 pub use rainier_broadcast::{Broadcastable, Broadcasting, Channel as BroadcastChannelName};
228 pub use rainier_events::Dispatcher;
229 pub use rainier_mail::{Content, Envelope, MailDriver, Mailable};
230 pub use rainier_metrics::{Metrics, RecordMetrics};
231 pub use rainier_notify::{Channels, Notifiable, Notification, Notifier};
232 pub use rainier_queue::{Job, JobContext, QueueDriver, QueueManager};
233 pub use rainier_telemetry::Trace;
234 pub use rainier_view::View as ViewData;
235 pub use rainier_websocket::{Rooms, Socket, WebSocketHandler, WebSocketRoutes};
236
237 pub use async_trait::async_trait;
238 pub use std::sync::Arc;
239}
240
241#[cfg(test)]
242mod tests {
243 use crate::prelude::*;
244 use rainier_http::Method;
245
246 #[tokio::test]
247 async fn an_application_boots_and_serves() {
248 let app = Rainier::new(".")
249 .without_facades()
250 .without_tracing()
251 .with_routes(|router| {
252 router.get("/", || async { "hello" }).name("home");
253 })
254 .boot()
255 .await
256 .expect("boots");
257
258 let kernel = app.resolve::<rainier_server::Kernel>().unwrap();
259 let response =
260 kernel.handle_request(Request::builder().method(Method::GET).uri("/").build()).await;
261
262 assert_eq!(response.status(), StatusCode::OK);
263 }
264
265 #[tokio::test]
266 async fn the_prelude_covers_a_realistic_controller() {
267 // A compile-level assertion that the prelude is actually sufficient:
268 // this uses an extractor, a form request, a response and the error
269 // type without importing anything else.
270 #[derive(serde::Deserialize)]
271 struct StorePost {
272 title: String,
273 }
274
275 #[async_trait]
276 impl FormRequest for StorePost {
277 fn rules() -> RuleSet {
278 vec![field("title", [Rule::Required, Rule::String])]
279 }
280 }
281
282 async fn store(Validated(post): Validated<StorePost>) -> Result<Response> {
283 Ok(Response::json(&serde_json::json!({ "title": post.title })))
284 }
285
286 let app = Rainier::new(".")
287 .without_facades()
288 .without_tracing()
289 .with_routes(|router| {
290 router.post("/posts", store);
291 })
292 .boot()
293 .await
294 .expect("boots");
295
296 let response = app
297 .resolve::<rainier_server::Kernel>()
298 .unwrap()
299 .handle_request(
300 Request::builder()
301 .method(Method::POST)
302 .uri("/posts")
303 .json(&serde_json::json!({ "title": "Hello" }))
304 .build(),
305 )
306 .await;
307
308 assert_eq!(response.status(), StatusCode::OK);
309 }
310
311 #[tokio::test]
312 async fn a_failed_contract_is_a_422() {
313 #[derive(serde::Deserialize)]
314 struct StorePost {
315 #[allow(dead_code)]
316 title: String,
317 }
318
319 #[async_trait]
320 impl FormRequest for StorePost {
321 fn rules() -> RuleSet {
322 vec![field("title", [Rule::Required])]
323 }
324 }
325
326 async fn store(Validated(_): Validated<StorePost>) -> &'static str {
327 "created"
328 }
329
330 let app = Rainier::new(".")
331 .without_facades()
332 .without_tracing()
333 .with_routes(|router| {
334 router.post("/posts", store);
335 })
336 .boot()
337 .await
338 .unwrap();
339
340 let response = app
341 .resolve::<rainier_server::Kernel>()
342 .unwrap()
343 .handle_request(
344 Request::builder()
345 .method(Method::POST)
346 .uri("/posts")
347 .header("accept", "application/json")
348 .json(&serde_json::json!({}))
349 .build(),
350 )
351 .await;
352
353 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
354 }
355}