Skip to main content

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}