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::{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), ou
67/// desabilite as três via [`without_docs`](Self::without_docs).
68pub struct App {
69    router: Router<Container>,
70    container: Container,
71    openapi: OpenApiState,
72    openapi_path: &'static str,
73    docs_path: &'static str,
74    redoc_path: &'static str,
75    interceptors: Vec<RouterMutator>,
76    docs_enabled: bool,
77    // TypeId::of::<EventHandlerRegistry<E>>() → Box<EventHandlerRegistry<E>>
78    event_registries: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
79}
80
81impl App {
82    /// Cria um App vazio com defaults: `/openapi.json`, `/docs`, `/redoc`.
83    pub fn new() -> Self {
84        Self {
85            router: Router::new(),
86            container: Container::new(),
87            openapi: OpenApiState::default(),
88            openapi_path: "/openapi.json",
89            docs_path: "/docs",
90            redoc_path: "/redoc",
91            interceptors: Vec::new(),
92            docs_enabled: true,
93            event_registries: HashMap::new(),
94        }
95    }
96
97    /// Customiza `title` e `version` do documento OpenAPI gerado.
98    pub fn openapi_info(mut self, title: impl Into<String>, version: impl Into<String>) -> Self {
99        self.openapi.set_info(title, version);
100        self
101    }
102
103    /// Registra um schema `T: ToSchema` em `components.schemas` do OpenAPI.
104    pub fn register_schema<T: ToSchema + PartialSchema>(mut self) -> Self {
105        self.openapi.register_schema::<T>();
106        self
107    }
108
109    /// Customiza o path em que o Scalar API Reference é servido (default `/docs`).
110    pub fn docs(mut self, path: &'static str) -> Self {
111        self.docs_path = path;
112        self
113    }
114
115    /// Customiza o path em que o ReDoc é servido (default `/redoc`).
116    pub fn redoc(mut self, path: &'static str) -> Self {
117        self.redoc_path = path;
118        self
119    }
120
121    /// Desabilita o registro de `/openapi.json`, `/docs` e `/redoc` em
122    /// [`Self::into_router`]. Útil para serviços internos que não querem
123    /// expor essa superfície (ex.: atrás de API Gateway com API key).
124    pub fn without_docs(mut self) -> Self {
125        self.docs_enabled = false;
126        self
127    }
128
129    /// Registra um service com lifetime Singleton no container.
130    ///
131    /// `T` pode ser `dyn Trait`: `app.provide::<dyn MyService>(Arc::new(impl))`.
132    /// Handlers extraem o serviço via `State<Arc<dyn MyService>>`.
133    pub fn provide<T: ?Sized + Send + Sync + 'static>(mut self, value: Arc<T>) -> Self {
134        self.container.insert(value);
135        self
136    }
137
138    /// API de teste: substitui o provider de `T` por uma instância mock.
139    /// `override` é palavra reservada — chame como `app.r#override::<...>(...)`.
140    pub fn r#override<T: ?Sized + Send + Sync + 'static>(mut self, value: Arc<T>) -> Self {
141        self.container.insert(value);
142        self
143    }
144
145    /// Registra um interceptor (tower middleware) sobre as rotas do usuário.
146    ///
147    /// Aplicado em [`Self::into_router`] apenas às rotas registradas via
148    /// [`Self::route`] — as rotas de documentação (`/openapi.json`, `/docs`,
149    /// `/redoc`) ficam de fora intencionalmente, para que não dependam da
150    /// pipeline de negócio (ex.: autenticação, rate limiting).
151    pub fn interceptor<I: Interceptor>(mut self, interceptor: I) -> Self {
152        let interceptor = std::sync::Arc::new(interceptor);
153        let mutator: RouterMutator = Box::new(move |router: Router<Container>| {
154            let interceptor = interceptor.clone();
155            let layer = axum::middleware::from_fn(move |req: Request, next: Next| {
156                let interceptor = interceptor.clone();
157                async move { interceptor.intercept(req, next).await }
158            });
159            router.layer(layer)
160        });
161        self.interceptors.push(mutator);
162        self
163    }
164
165    /// Registra um `tower::Layer` genérico sobre as rotas do usuário — mesmo
166    /// mecanismo de [`Self::interceptor`] (não afeta `/openapi.json`, `/docs`
167    /// nem `/redoc`), mas aceita qualquer `Layer` do ecossistema tower/axum
168    /// (ex.: `axum::extract::DefaultBodyLimit`, CORS, timeout, compressão),
169    /// em vez de exigir a trait `Interceptor` do framework.
170    pub fn layer<L>(mut self, layer: L) -> Self
171    where
172        L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
173        L::Service: tower::Service<Request> + Clone + Send + Sync + 'static,
174        <L::Service as tower::Service<Request>>::Response: IntoResponse + 'static,
175        <L::Service as tower::Service<Request>>::Error: Into<std::convert::Infallible> + 'static,
176        <L::Service as tower::Service<Request>>::Future: Send + 'static,
177    {
178        let mutator: RouterMutator = Box::new(move |router: Router<Container>| router.layer(layer));
179        self.interceptors.push(mutator);
180        self
181    }
182
183    /// Injeta uma [`ServerustConfig`] tipada no container. Handlers podem extraí-la via
184    /// `State<Arc<ServerustConfig>>`.
185    pub fn config(self, cfg: ServerustConfig) -> Self {
186        self.provide::<ServerustConfig>(Arc::new(cfg))
187    }
188
189    /// Registra um handler anotado por `#[get]`, `#[post]`, etc.
190    pub fn route<R: IntoRoute>(mut self, handler: R) -> Self {
191        let route = handler.into_route();
192        self.openapi
193            .push_operation(route.path, route.method, route.operation);
194        self.router = self.router.route(route.path, route.method_router);
195        self
196    }
197
198    /// Registra um [`EventHandler<E>`] tipado.
199    ///
200    /// Múltiplos handlers para o mesmo tipo `E` são acumulados e executados em
201    /// sequência por [`EventDispatcher::dispatch_event`]. O [`Container`] é
202    /// compartilhado entre handlers HTTP e event handlers — os mesmos serviços
203    /// registrados via [`Self::provide`] ficam disponíveis em `ctx`.
204    pub fn event<E, H>(mut self, handler: H) -> Self
205    where
206        E: Send + 'static,
207        H: EventHandler<E>,
208    {
209        let key = TypeId::of::<EventHandlerRegistry<E>>();
210        let registry = self
211            .event_registries
212            .entry(key)
213            .or_insert_with(|| Box::new(EventHandlerRegistry::<E>::new()));
214        registry
215            .downcast_mut::<EventHandlerRegistry<E>>()
216            .expect("type invariant: key matches registry type")
217            .register(handler);
218        self
219    }
220
221    /// Retorna `true` se pelo menos um event handler foi registrado (qualquer tipo `E`).
222    pub fn has_event_handlers(&self) -> bool {
223        !self.event_registries.is_empty()
224    }
225
226    /// Constrói um [`EventDispatcher<E>`] com todos os handlers registrados
227    /// para o tipo `E` e uma cópia do [`Container`] compartilhado.
228    pub fn into_event_dispatcher<E: Send + 'static>(mut self) -> EventDispatcher<E> {
229        let key = TypeId::of::<EventHandlerRegistry<E>>();
230        let registry = self
231            .event_registries
232            .remove(&key)
233            .and_then(|b| b.downcast::<EventHandlerRegistry<E>>().ok())
234            .unwrap_or_else(|| Box::new(EventHandlerRegistry::<E>::new()));
235        registry.into_dispatcher(self.container)
236    }
237
238    /// Constrói o `axum::Router` final adicionando `/openapi.json`, `/docs` e
239    /// `/redoc` — a menos que [`Self::without_docs`] tenha sido chamado.
240    pub fn into_router(self) -> Router {
241        // Aplica interceptors sobre as rotas do usuário antes de juntar com as
242        // rotas de documentação — isto garante que /openapi.json, /docs e
243        // /redoc NÃO sejam envolvidos pela pipeline de middleware do usuário.
244        let mut user_router = self.router;
245        for mutator in self.interceptors {
246            user_router = mutator(user_router);
247        }
248
249        if !self.docs_enabled {
250            return user_router.with_state(self.container);
251        }
252
253        let doc = self.openapi.build();
254        let json = doc.to_json().unwrap_or_else(|_| "{}".to_string());
255        let swagger_html = swagger_ui_html(self.openapi_path);
256        let redoc_page = redoc_html(self.openapi_path);
257
258        user_router
259            .route(
260                self.openapi_path,
261                get(move || {
262                    let json = json.clone();
263                    async move {
264                        (
265                            [(header::CONTENT_TYPE, "application/json")],
266                            json,
267                        )
268                            .into_response()
269                    }
270                }),
271            )
272            .route(
273                self.docs_path,
274                get(move || {
275                    let html = swagger_html.clone();
276                    async move {
277                        (
278                            [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
279                            html,
280                        )
281                            .into_response()
282                    }
283                }),
284            )
285            .route(
286                self.redoc_path,
287                get(move || {
288                    let html = redoc_page.clone();
289                    async move {
290                        (
291                            [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
292                            html,
293                        )
294                            .into_response()
295                    }
296                }),
297            )
298            .with_state(self.container)
299    }
300
301    /// Sobe um servidor HTTP local ligado em `addr` (ex.: `"127.0.0.1:3000"`).
302    ///
303    /// Imprime no stderr o endereço efetivo + URLs de documentação assim que o
304    /// listener fica pronto, para o desenvolvedor saber onde conectar:
305    ///
306    /// ```text
307    ///   🦀 serverust on http://0.0.0.0:3000
308    ///      docs:    http://0.0.0.0:3000/docs
309    ///      openapi: http://0.0.0.0:3000/openapi.json
310    /// ```
311    pub async fn run_http<A: ToSocketAddrs>(self, addr: A) -> std::io::Result<()> {
312        let listener = TcpListener::bind(addr).await?;
313        let local = listener.local_addr()?;
314        let docs_path = self.docs_path;
315        let openapi_path = self.openapi_path;
316        let router = self.into_router();
317        eprintln!();
318        eprintln!("  🦀 serverust on http://{local}");
319        eprintln!("     docs:    http://{local}{docs_path}");
320        eprintln!("     openapi: http://{local}{openapi_path}");
321        eprintln!();
322        axum::serve(listener, router).await
323    }
324}
325
326impl Default for App {
327    fn default() -> Self {
328        Self::new()
329    }
330}