Skip to main content

serverust_core/
app.rs

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