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 health;
118pub mod keys;
119pub mod limits;
120pub mod notifications;
121pub mod observability;
122pub mod queued_listeners;
123pub mod scheduling;
124pub mod signed;
125pub mod testing;
126
127pub use bootstrap::Rainier;
128pub use commands::{
129 console, KeyGenerateCommand, MigrateCommand, MigrateRollbackCommand, QueueWorkCommand,
130 RouteListCommand, ServeCommand,
131};
132pub use facades::Views;
133pub use health::{Health, Report as HealthReport};
134pub use limits::RateLimits;
135pub use queued_listeners::{DispatcherExt, FromEvent};
136pub use scheduling::{
137 CommandTask, JobTask, ScheduleExt, ScheduleListCommand, ScheduleRunCommand, ScheduleWorkCommand,
138};
139pub use signed::{SignedUrls, ValidateSignature};
140pub use testing::{TestApp, TestResponse};
141
142// --- the components, re-exported under short names -------------------------
143
144pub use rainier_auth as auth;
145pub use rainier_broadcast as broadcast;
146pub use rainier_cache as cache;
147pub use rainier_config as config;
148pub use rainier_console as console_kernel;
149pub use rainier_container as container;
150pub use rainier_crypt as crypt;
151pub use rainier_database as database;
152pub use rainier_drivers as drivers;
153pub use rainier_events as events;
154pub use rainier_filesystem as filesystem;
155pub use rainier_http as http;
156pub use rainier_http_client as http_client;
157pub use rainier_mail as mail;
158pub use rainier_metrics as metrics;
159pub use rainier_middleware as middleware;
160pub use rainier_notify as notify;
161pub use rainier_openapi as openapi;
162pub use rainier_queue as queue;
163pub use rainier_routing as routing;
164pub use rainier_scheduler as scheduler;
165pub use rainier_server as server;
166pub use rainier_session as session;
167pub use rainier_support as support;
168pub use rainier_telemetry as telemetry;
169pub use rainier_validation as validation;
170pub use rainier_view as view;
171pub use rainier_websocket as websocket;
172
173/// The ORM, so an application depends on one Rainier ORM version.
174pub use rainier_orm;
175
176// The macros are exported at the crate root by `#[macro_export]`, so they are
177// already reachable as `rainier_framework::facade!` and `rainier_framework::bind_executor!`.
178pub use rainier_container::{facade, Application, Facade, ServiceProvider};
179pub use rainier_database::bind_executor;
180
181/// Everything an application file usually wants in scope.
182///
183/// ```
184/// use rainier_framework::prelude::*;
185/// ```
186pub mod prelude {
187 pub use crate::bootstrap::Rainier;
188 pub use crate::facades::{
189 Broadcast, Cache, Config, Crypt, Event, Mail, Middleware, Notify, Queue, Session, Storage,
190 Url, View, Views, DB,
191 };
192
193 pub use rainier_container::{Application, Container, Facade, ServiceProvider};
194 pub use rainier_support::{build_info, BuildInfo, Context as _, Error, ErrorKind, Result};
195
196 // Typed configuration: a `Key<T>` for the path, a `Setting` for the value.
197 // The macros live at their crates' roots, so they are reachable as
198 // `rainier_framework::config::config_keys!` without being imported.
199 pub use rainier_config::{AppEnv, Key as ConfigurationKey, Setting as _};
200 pub use rainier_telemetry::LogFormat;
201
202 pub use crate::binding::{Bound, BoundAs};
203 pub use rainier_http::extract::{Bearer, Form, Json, Path, Query, RawBody};
204 pub use rainier_http::{
205 Cookie, FromRequest, Html, IntoResponse, Method, Redirect, Request, Response, StatusCode,
206 };
207
208 pub use rainier_cache::{Cache as CacheContract, CacheDriver, CacheExt as _, CacheManager};
209 pub use rainier_crypt::{EncrypterExt as _, Encryption, SignerExt as _};
210 pub use rainier_filesystem::{
211 Filesystem, FilesystemDriver, FilesystemExt as _, LocalFilesystem,
212 Storage as StorageManager,
213 };
214 pub use rainier_http_client::{Backoff, Http};
215 pub use rainier_middleware::{
216 Compress, MethodOverride, Middleware as MiddlewareContract, Next, Timeout, TrustProxies,
217 };
218 pub use rainier_routing::{
219 GroupAttributes, ParamConstraint, Req, ResourceController, Route, Router,
220 };
221 pub use rainier_session::{
222 Session as SessionBag, SessionDriver, SessionManager, SessionRequestExt as _, StartSession,
223 };
224 pub use rainier_validation::{field, FormRequest, Rule, RuleSet, Validated, Validator};
225
226 pub use rainier_database::{
227 BelongsTo, BelongsToMany, Criteria, Database, EntityRepository, HasMany, HasOne, Model,
228 Paginated, Related, RelatedCounts, Relation, Repository,
229 };
230 pub use rainier_orm::Entity;
231
232 pub use rainier_auth::{
233 AuthManager, Authenticatable, AuthenticatedUser, Credentials, Gate, GuardExt as _,
234 };
235 pub use rainier_broadcast::{Broadcastable, Broadcasting, Channel as BroadcastChannelName};
236 pub use rainier_events::Dispatcher;
237 pub use rainier_mail::{Content, Envelope, MailDriver, Mailable};
238 pub use rainier_metrics::{Metrics, RecordMetrics};
239 pub use rainier_notify::{Channels, Notifiable, Notification, Notifier};
240 pub use rainier_queue::{Job, JobContext, QueueDriver, QueueManager};
241 pub use rainier_telemetry::Trace;
242 pub use rainier_view::View as ViewData;
243 pub use rainier_websocket::{Rooms, Socket, WebSocketHandler, WebSocketRoutes};
244
245 pub use async_trait::async_trait;
246 pub use std::sync::Arc;
247}
248
249#[cfg(test)]
250mod tests {
251 use crate::prelude::*;
252 use rainier_http::Method;
253
254 #[tokio::test]
255 async fn an_application_boots_and_serves() {
256 let app = Rainier::new(".")
257 .without_facades()
258 .without_tracing()
259 .with_routes(|router| {
260 router.get("/", || async { "hello" }).name("home");
261 })
262 .boot()
263 .await
264 .expect("boots");
265
266 let kernel = app.resolve::<rainier_server::Kernel>().unwrap();
267 let response =
268 kernel.handle_request(Request::builder().method(Method::GET).uri("/").build()).await;
269
270 assert_eq!(response.status(), StatusCode::OK);
271 }
272
273 #[tokio::test]
274 async fn the_prelude_covers_a_realistic_controller() {
275 // A compile-level assertion that the prelude is actually sufficient:
276 // this uses an extractor, a form request, a response and the error
277 // type without importing anything else.
278 #[derive(serde::Deserialize)]
279 struct StorePost {
280 title: String,
281 }
282
283 #[async_trait]
284 impl FormRequest for StorePost {
285 fn rules() -> RuleSet {
286 vec![field("title", [Rule::Required, Rule::String])]
287 }
288 }
289
290 async fn store(Validated(post): Validated<StorePost>) -> Result<Response> {
291 Ok(Response::json(&serde_json::json!({ "title": post.title })))
292 }
293
294 let app = Rainier::new(".")
295 .without_facades()
296 .without_tracing()
297 .with_routes(|router| {
298 router.post("/posts", store);
299 })
300 .boot()
301 .await
302 .expect("boots");
303
304 let response = app
305 .resolve::<rainier_server::Kernel>()
306 .unwrap()
307 .handle_request(
308 Request::builder()
309 .method(Method::POST)
310 .uri("/posts")
311 .json(&serde_json::json!({ "title": "Hello" }))
312 .build(),
313 )
314 .await;
315
316 assert_eq!(response.status(), StatusCode::OK);
317 }
318
319 #[tokio::test]
320 async fn a_failed_contract_is_a_422() {
321 #[derive(serde::Deserialize)]
322 struct StorePost {
323 #[allow(dead_code)]
324 title: String,
325 }
326
327 #[async_trait]
328 impl FormRequest for StorePost {
329 fn rules() -> RuleSet {
330 vec![field("title", [Rule::Required])]
331 }
332 }
333
334 async fn store(Validated(_): Validated<StorePost>) -> &'static str {
335 "created"
336 }
337
338 let app = Rainier::new(".")
339 .without_facades()
340 .without_tracing()
341 .with_routes(|router| {
342 router.post("/posts", store);
343 })
344 .boot()
345 .await
346 .unwrap();
347
348 let response = app
349 .resolve::<rainier_server::Kernel>()
350 .unwrap()
351 .handle_request(
352 Request::builder()
353 .method(Method::POST)
354 .uri("/posts")
355 .header("accept", "application/json")
356 .json(&serde_json::json!({}))
357 .build(),
358 )
359 .await;
360
361 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
362 }
363}