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::openapi::HttpMethod;
13use utoipa::{PartialSchema, ToSchema};
14
15use crate::auth::{AllowUnannotated, AuthGate};
16use crate::config::ServerustConfig;
17use crate::container::Container;
18use crate::events::{EventDispatcher, EventHandler, EventHandlerRegistry};
19use crate::openapi::{OpenApiState, SegurancaDaRota, redoc_html, swagger_ui_html};
20use crate::pipeline::Interceptor;
21use crate::route::IntoRoute;
22
23type RouterMutator = Box<dyn FnOnce(Router<Container>) -> Router<Container> + Send + Sync>;
24
25/// Builder principal do framework.
26///
27/// Acumula rotas, services de DI, configuração de OpenAPI e middleware
28/// (interceptors). Use [`run_http`](Self::run_http) para servir local, ou a
29/// trait `AppRuntime` (do crate `serverust-lambda`) para o método `.run()`
30/// que detecta automaticamente entre Lambda e HTTP local.
31///
32/// # Exemplo
33///
34/// ```no_run
35/// use std::sync::Arc;
36/// use serverust_core::App;
37/// use serverust_macros::{get, injectable};
38///
39/// #[injectable]
40/// struct Greeter;
41///
42/// impl Greeter {
43/// fn hi(&self) -> String { "hello".into() }
44/// }
45///
46/// #[get("/")]
47/// async fn root(
48/// axum::extract::State(g): axum::extract::State<Arc<Greeter>>,
49/// ) -> String {
50/// g.hi()
51/// }
52///
53/// #[tokio::main]
54/// async fn main() -> std::io::Result<()> {
55/// App::new()
56/// .openapi_info("My API", "0.1.0")
57/// .provide::<Greeter>(Arc::new(Greeter))
58/// .route(root)
59/// .run_http("127.0.0.1:3000")
60/// .await
61/// }
62/// ```
63///
64/// # Rotas de documentação
65///
66/// [`into_router`](Self::into_router) injeta automaticamente três rotas:
67/// `/openapi.json` (OpenAPI 3.1), `/docs` (Scalar API Reference) e `/redoc` (ReDoc).
68/// Customize os paths via [`docs`](Self::docs) e [`redoc`](Self::redoc), ou
69/// desabilite as três via [`without_docs`](Self::without_docs).
70pub struct App {
71 router: Router<Container>,
72 container: Container,
73 openapi: OpenApiState,
74 openapi_path: &'static str,
75 docs_path: &'static str,
76 redoc_path: &'static str,
77 interceptors: Vec<RouterMutator>,
78 docs_enabled: bool,
79 /// Se [`Self::auth`] foi usado. Só serve para decidir se o log de
80 /// inicialização sai: sem autenticação instalada, listar rotas públicas
81 /// seria ruído — todas são.
82 auth_installed: bool,
83 /// Se [`Self::allow_unannotated`] foi usado.
84 allow_unannotated: bool,
85 /// Rotas marcadas públicas, na ordem de registro, para o log de init.
86 /// O método vem como `&'static str` (`"GET"`) porque o `HttpMethod` do
87 /// utoipa não é `Debug` nem `PartialEq` — inutilizável numa asserção de
88 /// teste, que é metade do propósito deste inventário.
89 public_routes: Vec<(&'static str, &'static str)>,
90 // TypeId::of::<EventHandlerRegistry<E>>() → Box<EventHandlerRegistry<E>>
91 event_registries: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
92}
93
94impl App {
95 /// Cria um App vazio com defaults: `/openapi.json`, `/docs`, `/redoc`.
96 pub fn new() -> Self {
97 Self {
98 router: Router::new(),
99 container: Container::new(),
100 openapi: OpenApiState::default(),
101 openapi_path: "/openapi.json",
102 docs_path: "/docs",
103 redoc_path: "/redoc",
104 interceptors: Vec::new(),
105 docs_enabled: true,
106 auth_installed: false,
107 allow_unannotated: false,
108 public_routes: Vec::new(),
109 event_registries: HashMap::new(),
110 }
111 }
112
113 /// Customiza `title` e `version` do documento OpenAPI gerado.
114 pub fn openapi_info(mut self, title: impl Into<String>, version: impl Into<String>) -> Self {
115 self.openapi.set_info(title, version);
116 self
117 }
118
119 /// Registra um schema `T: ToSchema` em `components.schemas` do OpenAPI.
120 pub fn register_schema<T: ToSchema + PartialSchema>(mut self) -> Self {
121 self.openapi.register_schema::<T>();
122 self
123 }
124
125 /// Customiza o path em que o Scalar API Reference é servido (default `/docs`).
126 pub fn docs(mut self, path: &'static str) -> Self {
127 self.docs_path = path;
128 self
129 }
130
131 /// Customiza o path em que o ReDoc é servido (default `/redoc`).
132 pub fn redoc(mut self, path: &'static str) -> Self {
133 self.redoc_path = path;
134 self
135 }
136
137 /// Desabilita o registro de `/openapi.json`, `/docs` e `/redoc` em
138 /// [`Self::into_router`]. Útil para serviços internos que não querem
139 /// expor essa superfície (ex.: atrás de API Gateway com API key).
140 pub fn without_docs(mut self) -> Self {
141 self.docs_enabled = false;
142 self
143 }
144
145 /// Registra um service com lifetime Singleton no container.
146 ///
147 /// `T` pode ser `dyn Trait`: `app.provide::<dyn MyService>(Arc::new(impl))`.
148 /// Handlers extraem o serviço via `State<Arc<dyn MyService>>`.
149 pub fn provide<T: ?Sized + Send + Sync + 'static>(mut self, value: Arc<T>) -> Self {
150 self.container.insert(value);
151 self
152 }
153
154 /// API de teste: substitui o provider de `T` por uma instância mock.
155 /// `override` é palavra reservada — chame como `app.r#override::<...>(...)`.
156 pub fn r#override<T: ?Sized + Send + Sync + 'static>(mut self, value: Arc<T>) -> Self {
157 self.container.insert(value);
158 self
159 }
160
161 /// Registra um interceptor (tower middleware) sobre as rotas do usuário.
162 ///
163 /// Aplicado em [`Self::into_router`] apenas às rotas registradas via
164 /// [`Self::route`] — as rotas de documentação (`/openapi.json`, `/docs`,
165 /// `/redoc`) ficam de fora intencionalmente, para que não dependam da
166 /// pipeline de negócio (ex.: autenticação, rate limiting).
167 pub fn interceptor<I: Interceptor>(mut self, interceptor: I) -> Self {
168 let interceptor = std::sync::Arc::new(interceptor);
169 let mutator: RouterMutator = Box::new(move |router: Router<Container>| {
170 let interceptor = interceptor.clone();
171 let layer = axum::middleware::from_fn(move |req: Request, next: Next| {
172 let interceptor = interceptor.clone();
173 async move { interceptor.intercept(req, next).await }
174 });
175 router.layer(layer)
176 });
177 self.interceptors.push(mutator);
178 self
179 }
180
181 /// Registra um `tower::Layer` genérico sobre as rotas do usuário — mesmo
182 /// mecanismo de [`Self::interceptor`] (não afeta `/openapi.json`, `/docs`
183 /// nem `/redoc`), mas aceita qualquer `Layer` do ecossistema tower/axum
184 /// (ex.: `axum::extract::DefaultBodyLimit`, CORS, timeout, compressão),
185 /// em vez de exigir a trait `Interceptor` do framework.
186 pub fn layer<L>(mut self, layer: L) -> Self
187 where
188 L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
189 L::Service: tower::Service<Request> + Clone + Send + Sync + 'static,
190 <L::Service as tower::Service<Request>>::Response: IntoResponse + 'static,
191 <L::Service as tower::Service<Request>>::Error: Into<std::convert::Infallible> + 'static,
192 <L::Service as tower::Service<Request>>::Future: Send + 'static,
193 {
194 let mutator: RouterMutator = Box::new(move |router: Router<Container>| router.layer(layer));
195 self.interceptors.push(mutator);
196 self
197 }
198
199 /// Instala autenticação — o mesmo que [`Self::layer`], com duas
200 /// diferenças que importam.
201 ///
202 /// ```ignore
203 /// App::new()
204 /// .auth(AuthLayer::<StandardClaims>::new(jwt))
205 /// .route(me)
206 /// ```
207 ///
208 /// A primeira é o nome: `.layer(AuthLayer::new(...))` esconde num ponto
209 /// de extensão genérico a decisão mais consequente do serviço — a partir
210 /// dali **toda rota não marcada `#[public]` passa a exigir identidade**.
211 /// Quem lê o `main` merece ver isso escrito.
212 ///
213 /// A segunda é o log de inicialização: com `.auth(...)`, o
214 /// [`Self::into_router`] imprime no stderr a lista de rotas públicas do
215 /// serviço. É a mitigação 2 da decisão 5 da ADR 0009 — a superfície
216 /// anônima fica visível em cada deploy, não só para quem roda `grep`.
217 ///
218 /// `.layer(AuthLayer::new(...))` continua funcionando e continua ativando
219 /// o default deny (quem faz isso são os marcadores que o layer insere,
220 /// não este método). O que se perde é a lista no boot.
221 pub fn auth<L>(mut self, layer: L) -> Self
222 where
223 L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
224 L::Service: tower::Service<Request> + Clone + Send + Sync + 'static,
225 <L::Service as tower::Service<Request>>::Response: IntoResponse + 'static,
226 <L::Service as tower::Service<Request>>::Error: Into<std::convert::Infallible> + 'static,
227 <L::Service as tower::Service<Request>>::Future: Send + 'static,
228 {
229 self.auth_installed = true;
230 self.layer(layer)
231 }
232
233 /// Desliga o default deny: rota sem `#[public]` volta a responder sem
234 /// identidade. **Escape hatch de migração, não configuração.**
235 ///
236 /// Instalar autenticação num serviço existente fecha todas as rotas de
237 /// uma vez, e marcar dezenas de `#[public]` num único PR é exatamente
238 /// onde o erro entra. Esta linha compra tempo para fazer isso rota a
239 /// rota, com a suíte de testes como checklist.
240 ///
241 /// É uma linha visível no builder em vez de omissão distribuída por N
242 /// rotas — e o log de init a denuncia em voz alta a cada boot, porque uma
243 /// permanência silenciosa aqui é o mesmo buraco que o default deny
244 /// fecha.
245 ///
246 /// Não afeta `#[authorize]`: afrouxar o default não é abrir mão do que
247 /// foi pedido de propósito.
248 pub fn allow_unannotated(mut self) -> Self {
249 self.allow_unannotated = true;
250 self.layer(axum::Extension(AllowUnannotated))
251 }
252
253 /// As rotas marcadas públicas, na ordem de registro.
254 ///
255 /// Mesmo conteúdo do log de inicialização, para quem queira afirmar a
256 /// superfície anônima num teste em vez de confiar na leitura do log.
257 pub fn public_routes(&self) -> &[(&'static str, &'static str)] {
258 &self.public_routes
259 }
260
261 /// Injeta uma [`ServerustConfig`] tipada no container. Handlers podem extraí-la via
262 /// `State<Arc<ServerustConfig>>`.
263 pub fn config(self, cfg: ServerustConfig) -> Self {
264 self.provide::<ServerustConfig>(Arc::new(cfg))
265 }
266
267 /// Registra um handler anotado por `#[get]`, `#[post]`, etc.
268 ///
269 /// Rota que não seja marcada pública (via [`crate::Route::public`]) é
270 /// embrulhada pelo [`AuthGate`], que aplica o default deny da ADR 0009. O
271 /// portão é inerte enquanto não houver autenticação instalada, então a
272 /// ordem do builder é irrelevante: registrar a rota antes ou depois de
273 /// configurar autenticação dá o mesmo resultado.
274 pub fn route<R: IntoRoute>(mut self, handler: R) -> Self {
275 let route = handler.into_route();
276 let route_method = metodo_str(&route.method);
277
278 // O que o documento vai dizer sobre esta rota. Derivado das MESMAS
279 // marcações que decidem o comportamento em runtime — `is_public` e os
280 // escopos do `#[authorize]` — para que documento e portão não possam
281 // divergir sem alguém mexer nos dois.
282 let seguranca = if route.is_public {
283 SegurancaDaRota::Publica
284 } else if route.required_scopes.is_empty() {
285 SegurancaDaRota::ExigeIdentidade
286 } else {
287 SegurancaDaRota::ExigeEscopos(route.required_scopes)
288 };
289
290 self.openapi
291 .push_operation(route.path, route.method, route.operation, seguranca);
292
293 let method_router = if route.is_public {
294 self.public_routes.push((route_method, route.path));
295 route.method_router
296 } else {
297 route.method_router.layer(AuthGate)
298 };
299
300 self.router = self.router.route(route.path, method_router);
301 self
302 }
303
304 /// Registra um [`EventHandler<E>`] tipado.
305 ///
306 /// Múltiplos handlers para o mesmo tipo `E` são acumulados e executados em
307 /// sequência por [`EventDispatcher::dispatch_event`]. O [`Container`] é
308 /// compartilhado entre handlers HTTP e event handlers — os mesmos serviços
309 /// registrados via [`Self::provide`] ficam disponíveis em `ctx`.
310 pub fn event<E, H>(mut self, handler: H) -> Self
311 where
312 E: Send + 'static,
313 H: EventHandler<E>,
314 {
315 let key = TypeId::of::<EventHandlerRegistry<E>>();
316 let registry = self
317 .event_registries
318 .entry(key)
319 .or_insert_with(|| Box::new(EventHandlerRegistry::<E>::new()));
320 registry
321 .downcast_mut::<EventHandlerRegistry<E>>()
322 .expect("type invariant: key matches registry type")
323 .register(handler);
324 self
325 }
326
327 /// Retorna `true` se pelo menos um event handler foi registrado (qualquer tipo `E`).
328 pub fn has_event_handlers(&self) -> bool {
329 !self.event_registries.is_empty()
330 }
331
332 /// Constrói um [`EventDispatcher<E>`] com todos os handlers registrados
333 /// para o tipo `E` e uma cópia do [`Container`] compartilhado.
334 pub fn into_event_dispatcher<E: Send + 'static>(mut self) -> EventDispatcher<E> {
335 let key = TypeId::of::<EventHandlerRegistry<E>>();
336 let registry = self
337 .event_registries
338 .remove(&key)
339 .and_then(|b| b.downcast::<EventHandlerRegistry<E>>().ok())
340 .unwrap_or_else(|| Box::new(EventHandlerRegistry::<E>::new()));
341 registry.into_dispatcher(self.container)
342 }
343
344 /// Constrói o `axum::Router` final adicionando `/openapi.json`, `/docs` e
345 /// `/redoc` — a menos que [`Self::without_docs`] tenha sido chamado.
346 pub fn into_router(self) -> Router {
347 if self.auth_installed {
348 eprint!(
349 "{}",
350 relatorio_de_init(&self.public_routes, self.allow_unannotated)
351 );
352 }
353
354 // Aplica interceptors sobre as rotas do usuário antes de juntar com as
355 // rotas de documentação — isto garante que /openapi.json, /docs e
356 // /redoc NÃO sejam envolvidos pela pipeline de middleware do usuário.
357 let mut user_router = self.router;
358 for mutator in self.interceptors {
359 user_router = mutator(user_router);
360 }
361
362 if !self.docs_enabled {
363 return user_router.with_state(self.container);
364 }
365
366 let doc = self.openapi.build(self.auth_installed);
367 let json = doc.to_json().unwrap_or_else(|_| "{}".to_string());
368 let swagger_html = swagger_ui_html(self.openapi_path);
369 let redoc_page = redoc_html(self.openapi_path);
370
371 user_router
372 .route(
373 self.openapi_path,
374 get(move || {
375 let json = json.clone();
376 async move {
377 (
378 [(header::CONTENT_TYPE, "application/json")],
379 json,
380 )
381 .into_response()
382 }
383 }),
384 )
385 .route(
386 self.docs_path,
387 get(move || {
388 let html = swagger_html.clone();
389 async move {
390 (
391 [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
392 html,
393 )
394 .into_response()
395 }
396 }),
397 )
398 .route(
399 self.redoc_path,
400 get(move || {
401 let html = redoc_page.clone();
402 async move {
403 (
404 [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
405 html,
406 )
407 .into_response()
408 }
409 }),
410 )
411 .with_state(self.container)
412 }
413
414 /// Sobe um servidor HTTP local ligado em `addr` (ex.: `"127.0.0.1:3000"`).
415 ///
416 /// Imprime no stderr o endereço efetivo + URLs de documentação assim que o
417 /// listener fica pronto, para o desenvolvedor saber onde conectar:
418 ///
419 /// ```text
420 /// 🦀 serverust on http://0.0.0.0:3000
421 /// docs: http://0.0.0.0:3000/docs
422 /// openapi: http://0.0.0.0:3000/openapi.json
423 /// ```
424 pub async fn run_http<A: ToSocketAddrs>(self, addr: A) -> std::io::Result<()> {
425 let listener = TcpListener::bind(addr).await?;
426 let local = listener.local_addr()?;
427 let docs_path = self.docs_path;
428 let openapi_path = self.openapi_path;
429 let router = self.into_router();
430 eprintln!();
431 eprintln!(" 🦀 serverust on http://{local}");
432 eprintln!(" docs: http://{local}{docs_path}");
433 eprintln!(" openapi: http://{local}{openapi_path}");
434 eprintln!();
435 axum::serve(listener, router).await
436 }
437}
438
439impl Default for App {
440 fn default() -> Self {
441 Self::new()
442 }
443}
444
445/// O texto do log de inicialização de autenticação (mitigação 2 da decisão 5
446/// da ADR 0009).
447///
448/// Separado do [`App::into_router`] e puro para ser afirmável em teste: o
449/// conteúdo desta lista é uma declaração de segurança, e declaração de
450/// segurança que ninguém verifica é a categoria de problema que a ADR inteira
451/// trata.
452///
453/// Lista **o que é aberto**, nunca o que é protegido. A lista curta é a que
454/// se lê; inverter isso produziria um log proporcional ao serviço, que
455/// ninguém lê — e a auditoria por presença vale também para a ferramenta de
456/// auditoria.
457fn relatorio_de_init(publicas: &[(&'static str, &'static str)], afrouxado: bool) -> String {
458 use std::fmt::Write as _;
459
460 let mut saida = String::new();
461
462 if afrouxado {
463 // Em voz alta e antes da lista: com o escape hatch ligado a lista
464 // abaixo é irrelevante, porque tudo está aberto.
465 saida.push_str(
466 "\n ⚠️ serverust: default deny DESLIGADO por .allow_unannotated()\n \
467 toda rota responde sem identidade. Isto é escape hatch de migração — \
468 marque as rotas com #[public] e remova a chamada.\n\n",
469 );
470 return saida;
471 }
472
473 let _ = writeln!(saida, "\n 🔒 serverust: default deny ativo");
474
475 if publicas.is_empty() {
476 let _ = writeln!(
477 saida,
478 " nenhuma rota pública — toda rota exige identidade"
479 );
480 } else {
481 let _ = writeln!(
482 saida,
483 " {} rota(s) pública(s), sem exigir identidade:",
484 publicas.len()
485 );
486 for (metodo, path) in publicas {
487 let _ = writeln!(saida, " {metodo} {path}");
488 }
489 }
490
491 saida.push('\n');
492 saida
493}
494
495/// Nome HTTP do método, em maiúsculas.
496fn metodo_str(metodo: &HttpMethod) -> &'static str {
497 match metodo {
498 HttpMethod::Get => "GET",
499 HttpMethod::Post => "POST",
500 HttpMethod::Put => "PUT",
501 HttpMethod::Patch => "PATCH",
502 HttpMethod::Delete => "DELETE",
503 HttpMethod::Head => "HEAD",
504 HttpMethod::Options => "OPTIONS",
505 HttpMethod::Trace => "TRACE",
506 }
507}
508
509#[cfg(test)]
510mod testes_do_relatorio {
511 use super::*;
512
513 #[test]
514 fn lista_as_rotas_publicas_com_metodo_e_path() {
515 let texto = relatorio_de_init(&[("GET", "/health"), ("POST", "/webhooks/stripe")], false);
516
517 assert!(texto.contains("default deny ativo"), "{texto}");
518 assert!(texto.contains("2 rota(s) pública(s)"), "{texto}");
519 assert!(texto.contains("GET /health"), "{texto}");
520 assert!(texto.contains("POST /webhooks/stripe"), "{texto}");
521 }
522
523 #[test]
524 fn sem_rotas_publicas_diz_isso_explicitamente() {
525 let texto = relatorio_de_init(&[], false);
526
527 assert!(texto.contains("nenhuma rota pública"), "{texto}");
528 }
529
530 /// Com o escape hatch ligado, a lista de públicas seria enganosa: tudo
531 /// está aberto. O relatório precisa dizer isso, e não listar.
532 #[test]
533 fn escape_hatch_avisa_em_vez_de_listar() {
534 let texto = relatorio_de_init(&[("GET", "/health")], true);
535
536 assert!(texto.contains("DESLIGADO"), "{texto}");
537 assert!(texto.contains("allow_unannotated"), "{texto}");
538 assert!(
539 !texto.contains("/health"),
540 "listar rotas públicas com o default deny desligado sugere que as \
541 demais estão protegidas — {texto}"
542 );
543 }
544}