Skip to main content

rainier_framework/
lib.rs

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