Skip to main content

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}