serverust_core/app.rs
1use std::any::{Any, TypeId};
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use axum::Router;
6use axum::extract::Request;
7use axum::http::header;
8use axum::middleware::Next;
9use axum::response::IntoResponse;
10use axum::routing::get;
11use tokio::net::{TcpListener, ToSocketAddrs};
12use utoipa::{PartialSchema, ToSchema};
13
14use crate::config::ServerustConfig;
15use crate::container::Container;
16use crate::events::{EventDispatcher, EventHandler, EventHandlerRegistry};
17use crate::openapi::{OpenApiState, redoc_html, swagger_ui_html};
18use crate::pipeline::Interceptor;
19use crate::route::IntoRoute;
20
21type RouterMutator = Box<dyn FnOnce(Router<Container>) -> Router<Container> + Send + Sync>;
22
23/// Builder principal do framework.
24///
25/// Acumula rotas, services de DI, configuração de OpenAPI e middleware
26/// (interceptors). Use [`run_http`](Self::run_http) para servir local, ou a
27/// trait `AppRuntime` (do crate `serverust-lambda`) para o método `.run()`
28/// que detecta automaticamente entre Lambda e HTTP local.
29///
30/// # Exemplo
31///
32/// ```no_run
33/// use std::sync::Arc;
34/// use serverust_core::App;
35/// use serverust_macros::{get, injectable};
36///
37/// #[injectable]
38/// struct Greeter;
39///
40/// impl Greeter {
41/// fn hi(&self) -> String { "hello".into() }
42/// }
43///
44/// #[get("/")]
45/// async fn root(
46/// axum::extract::State(g): axum::extract::State<Arc<Greeter>>,
47/// ) -> String {
48/// g.hi()
49/// }
50///
51/// #[tokio::main]
52/// async fn main() -> std::io::Result<()> {
53/// App::new()
54/// .openapi_info("My API", "0.1.0")
55/// .provide::<Greeter>(Arc::new(Greeter))
56/// .route(root)
57/// .run_http("127.0.0.1:3000")
58/// .await
59/// }
60/// ```
61///
62/// # Rotas de documentação
63///
64/// [`into_router`](Self::into_router) injeta automaticamente três rotas:
65/// `/openapi.json` (OpenAPI 3.1), `/docs` (Scalar API Reference) e `/redoc` (ReDoc).
66/// Customize os paths via [`docs`](Self::docs) e [`redoc`](Self::redoc).
67pub struct App {
68 router: Router<Container>,
69 container: Container,
70 openapi: OpenApiState,
71 openapi_path: &'static str,
72 docs_path: &'static str,
73 redoc_path: &'static str,
74 interceptors: Vec<RouterMutator>,
75 // TypeId::of::<EventHandlerRegistry<E>>() → Box<EventHandlerRegistry<E>>
76 event_registries: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
77}
78
79impl App {
80 /// Cria um App vazio com defaults: `/openapi.json`, `/docs`, `/redoc`.
81 pub fn new() -> Self {
82 Self {
83 router: Router::new(),
84 container: Container::new(),
85 openapi: OpenApiState::default(),
86 openapi_path: "/openapi.json",
87 docs_path: "/docs",
88 redoc_path: "/redoc",
89 interceptors: Vec::new(),
90 event_registries: HashMap::new(),
91 }
92 }
93
94 /// Customiza `title` e `version` do documento OpenAPI gerado.
95 pub fn openapi_info(mut self, title: impl Into<String>, version: impl Into<String>) -> Self {
96 self.openapi.set_info(title, version);
97 self
98 }
99
100 /// Registra um schema `T: ToSchema` em `components.schemas` do OpenAPI.
101 pub fn register_schema<T: ToSchema + PartialSchema>(mut self) -> Self {
102 self.openapi.register_schema::<T>();
103 self
104 }
105
106 /// Customiza o path em que o Scalar API Reference é servido (default `/docs`).
107 pub fn docs(mut self, path: &'static str) -> Self {
108 self.docs_path = path;
109 self
110 }
111
112 /// Customiza o path em que o ReDoc é servido (default `/redoc`).
113 pub fn redoc(mut self, path: &'static str) -> Self {
114 self.redoc_path = path;
115 self
116 }
117
118 /// Registra um service com lifetime Singleton no container.
119 ///
120 /// `T` pode ser `dyn Trait`: `app.provide::<dyn MyService>(Arc::new(impl))`.
121 /// Handlers extraem o serviço via `State<Arc<dyn MyService>>`.
122 pub fn provide<T: ?Sized + Send + Sync + 'static>(mut self, value: Arc<T>) -> Self {
123 self.container.insert(value);
124 self
125 }
126
127 /// API de teste: substitui o provider de `T` por uma instância mock.
128 /// `override` é palavra reservada — chame como `app.r#override::<...>(...)`.
129 pub fn r#override<T: ?Sized + Send + Sync + 'static>(mut self, value: Arc<T>) -> Self {
130 self.container.insert(value);
131 self
132 }
133
134 /// Registra um interceptor (tower middleware) sobre as rotas do usuário.
135 ///
136 /// Aplicado em [`Self::into_router`] apenas às rotas registradas via
137 /// [`Self::route`] — as rotas de documentação (`/openapi.json`, `/docs`,
138 /// `/redoc`) ficam de fora intencionalmente, para que não dependam da
139 /// pipeline de negócio (ex.: autenticação, rate limiting).
140 pub fn interceptor<I: Interceptor>(mut self, interceptor: I) -> Self {
141 let interceptor = std::sync::Arc::new(interceptor);
142 let mutator: RouterMutator = Box::new(move |router: Router<Container>| {
143 let interceptor = interceptor.clone();
144 let layer = axum::middleware::from_fn(move |req: Request, next: Next| {
145 let interceptor = interceptor.clone();
146 async move { interceptor.intercept(req, next).await }
147 });
148 router.layer(layer)
149 });
150 self.interceptors.push(mutator);
151 self
152 }
153
154 /// Injeta uma [`ServerustConfig`] tipada no container. Handlers podem extraí-la via
155 /// `State<Arc<ServerustConfig>>`.
156 pub fn config(self, cfg: ServerustConfig) -> Self {
157 self.provide::<ServerustConfig>(Arc::new(cfg))
158 }
159
160 /// Registra um handler anotado por `#[get]`, `#[post]`, etc.
161 pub fn route<R: IntoRoute>(mut self, handler: R) -> Self {
162 let route = handler.into_route();
163 self.openapi
164 .push_operation(route.path, route.method, route.operation);
165 self.router = self.router.route(route.path, route.method_router);
166 self
167 }
168
169 /// Registra um [`EventHandler<E>`] tipado.
170 ///
171 /// Múltiplos handlers para o mesmo tipo `E` são acumulados e executados em
172 /// sequência por [`EventDispatcher::dispatch_event`]. O [`Container`] é
173 /// compartilhado entre handlers HTTP e event handlers — os mesmos serviços
174 /// registrados via [`Self::provide`] ficam disponíveis em `ctx`.
175 pub fn event<E, H>(mut self, handler: H) -> Self
176 where
177 E: Send + 'static,
178 H: EventHandler<E>,
179 {
180 let key = TypeId::of::<EventHandlerRegistry<E>>();
181 let registry = self
182 .event_registries
183 .entry(key)
184 .or_insert_with(|| Box::new(EventHandlerRegistry::<E>::new()));
185 registry
186 .downcast_mut::<EventHandlerRegistry<E>>()
187 .expect("type invariant: key matches registry type")
188 .register(handler);
189 self
190 }
191
192 /// Retorna `true` se pelo menos um event handler foi registrado (qualquer tipo `E`).
193 pub fn has_event_handlers(&self) -> bool {
194 !self.event_registries.is_empty()
195 }
196
197 /// Constrói um [`EventDispatcher<E>`] com todos os handlers registrados
198 /// para o tipo `E` e uma cópia do [`Container`] compartilhado.
199 pub fn into_event_dispatcher<E: Send + 'static>(mut self) -> EventDispatcher<E> {
200 let key = TypeId::of::<EventHandlerRegistry<E>>();
201 let registry = self
202 .event_registries
203 .remove(&key)
204 .and_then(|b| b.downcast::<EventHandlerRegistry<E>>().ok())
205 .unwrap_or_else(|| Box::new(EventHandlerRegistry::<E>::new()));
206 registry.into_dispatcher(self.container)
207 }
208
209 /// Constrói o `axum::Router` final adicionando `/openapi.json`, `/docs` e `/redoc`.
210 pub fn into_router(self) -> Router {
211 let doc = self.openapi.build();
212 let json = doc.to_json().unwrap_or_else(|_| "{}".to_string());
213 let swagger_html = swagger_ui_html(self.openapi_path);
214 let redoc_page = redoc_html(self.openapi_path);
215
216 // Aplica interceptors sobre as rotas do usuário antes de juntar com as
217 // rotas de documentação — isto garante que /openapi.json, /docs e
218 // /redoc NÃO sejam envolvidos pela pipeline de middleware do usuário.
219 let mut user_router = self.router;
220 for mutator in self.interceptors {
221 user_router = mutator(user_router);
222 }
223
224 user_router
225 .route(
226 self.openapi_path,
227 get(move || {
228 let json = json.clone();
229 async move {
230 (
231 [(header::CONTENT_TYPE, "application/json")],
232 json,
233 )
234 .into_response()
235 }
236 }),
237 )
238 .route(
239 self.docs_path,
240 get(move || {
241 let html = swagger_html.clone();
242 async move {
243 (
244 [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
245 html,
246 )
247 .into_response()
248 }
249 }),
250 )
251 .route(
252 self.redoc_path,
253 get(move || {
254 let html = redoc_page.clone();
255 async move {
256 (
257 [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
258 html,
259 )
260 .into_response()
261 }
262 }),
263 )
264 .with_state(self.container)
265 }
266
267 /// Sobe um servidor HTTP local ligado em `addr` (ex.: `"127.0.0.1:3000"`).
268 ///
269 /// Imprime no stderr o endereço efetivo + URLs de documentação assim que o
270 /// listener fica pronto, para o desenvolvedor saber onde conectar:
271 ///
272 /// ```text
273 /// 🦀 serverust on http://0.0.0.0:3000
274 /// docs: http://0.0.0.0:3000/docs
275 /// openapi: http://0.0.0.0:3000/openapi.json
276 /// ```
277 pub async fn run_http<A: ToSocketAddrs>(self, addr: A) -> std::io::Result<()> {
278 let listener = TcpListener::bind(addr).await?;
279 let local = listener.local_addr()?;
280 let docs_path = self.docs_path;
281 let openapi_path = self.openapi_path;
282 let router = self.into_router();
283 eprintln!();
284 eprintln!(" 🦀 serverust on http://{local}");
285 eprintln!(" docs: http://{local}{docs_path}");
286 eprintln!(" openapi: http://{local}{openapi_path}");
287 eprintln!();
288 axum::serve(listener, router).await
289 }
290}
291
292impl Default for App {
293 fn default() -> Self {
294 Self::new()
295 }
296}