Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions crates/rustapi-core/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,14 @@ impl RustApi {
self
}

/// Add a UI route without registering OpenAPI operations.
///
/// Use this for SSR or UI-only endpoints that should not appear in OpenAPI.
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation for route_ui could be improved by adding an example section similar to the route method. Consider adding a code example showing how to use this method for UI/SSR endpoints.

Suggested change
/// Use this for SSR or UI-only endpoints that should not appear in OpenAPI.
/// Use this for SSR or UI-only endpoints that should not appear in OpenAPI.
///
/// # Example
///
/// ```rust,ignore
/// use rustapi_rs::prelude::*;
///
/// async fn ui_index() -> impl IntoResponse {
/// Html("<html><body><h1>Welcome</h1></body></html>")
/// }
///
/// #[tokio::main]
/// async fn main() -> Result<()> {
/// RustApi::new()
/// // API route registered in OpenAPI
/// .route("/api/health", get(health_check))
/// // UI route not included in OpenAPI
/// .route_ui("/", get(ui_index))
/// .run("127.0.0.1:8080")
/// .await
/// }
/// ```

Copilot uses AI. Check for mistakes.
pub fn route_ui(mut self, path: &str, method_router: MethodRouter) -> Self {
self.router = self.router.route(path, method_router);
self
}

/// Add a typed route
pub fn typed<P: crate::typed_path::TypedPath>(self, method_router: MethodRouter) -> Self {
self.route(P::PATH, method_router)
Expand Down Expand Up @@ -438,6 +446,22 @@ impl RustApi {
self.route_with_method(route.path, method_enum, route.handler)
}

/// Mount a UI route created with `#[rustapi::get]`, `#[rustapi::post]`, etc.
///
/// This skips OpenAPI registration while still mounting the handler.
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation for mount_route_ui could be improved by adding an example section similar to the mount_route method above. Consider adding a code example showing how to use this method with macro-generated routes.

Suggested change
/// This skips OpenAPI registration while still mounting the handler.
/// This skips OpenAPI registration while still mounting the handler.
///
/// # Example
///
/// ```rust,ignore
/// use rustapi_rs::prelude::*;
///
/// struct AppState;
///
/// #[rustapi::get("/ui")]
/// async fn ui_home(state: State<AppState>) -> impl IntoResponse {
/// // Render your UI here
/// "Hello from UI"
/// }
///
/// #[tokio::main]
/// async fn main() -> Result<()> {
/// RustApi::new()
/// .state(AppState)
/// // Mount a UI route without registering it in the OpenAPI spec
/// .mount_route_ui(route!(ui_home))
/// .run("127.0.0.1:8080")
/// .await
/// }
/// ```

Copilot uses AI. Check for mistakes.
pub fn mount_route_ui(self, route: crate::handler::Route) -> Self {
let method_enum = match route.method {
"GET" => http::Method::GET,
"POST" => http::Method::POST,
"PUT" => http::Method::PUT,
"DELETE" => http::Method::DELETE,
"PATCH" => http::Method::PATCH,
_ => http::Method::GET,
};
Comment on lines +453 to +460
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method-to-enum conversion logic is duplicated across multiple functions (mount_auto_routes_grouped, mount_route, and now mount_route_ui). Consider extracting this into a helper function to reduce duplication and ensure consistency in how method strings are converted to HTTP method enums.

Copilot uses AI. Check for mistakes.

self.route_with_method_ui(route.path, method_enum, route.handler)
}

/// Helper to mount a single method handler
fn route_with_method(
self,
Expand Down Expand Up @@ -475,6 +499,28 @@ impl RustApi {
self.route(&path, method_router)
}

/// Helper to mount a single method handler without OpenAPI registration
fn route_with_method_ui(
self,
path: &str,
method: http::Method,
handler: crate::handler::BoxedHandler,
) -> Self {
use crate::router::MethodRouter;

let path = if !path.starts_with('/') {
format!("/{}", path)
} else {
path.to_string()
};
Comment on lines +502 to +515
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper function duplicates the path normalization logic from route_with_method. Consider extracting the path normalization into a shared helper function to avoid code duplication and ensure consistency.

Copilot uses AI. Check for mistakes.

let mut handlers = std::collections::HashMap::new();
handlers.insert(method, handler);

let method_router = MethodRouter::from_boxed(handlers);
self.route_ui(&path, method_router)
}

/// Nest a router under a prefix
///
/// All routes from the nested router will be registered with the prefix
Expand Down Expand Up @@ -1035,6 +1081,7 @@ impl Default for RustApi {
mod tests {
use super::RustApi;
use crate::extract::{FromRequestParts, State};
use crate::handler::Route;
use crate::path_params::PathParams;
use crate::request::Request;
use crate::router::{get, post, Router};
Expand Down Expand Up @@ -1160,6 +1207,41 @@ mod tests {
}
}

#[test]
fn test_route_ui_excludes_openapi() {
async fn handler() -> &'static str {
"hello"
}

let app = RustApi::new().route_ui("/", get(handler));
assert!(
app.openapi_spec().paths.is_empty(),
"UI routes should not be added to OpenAPI spec"
);

let router = app.into_router();
let routes = router.registered_routes();
assert!(routes.contains_key("/"), "Expected UI route to be registered");
}

#[test]
fn test_mount_route_ui_excludes_openapi() {
async fn handler() -> &'static str {
"hello"
}

let route = Route::new("/", "GET", handler);
let app = RustApi::new().mount_route_ui(route);
assert!(
app.openapi_spec().paths.is_empty(),
"UI routes should not be added to OpenAPI spec"
);

let router = app.into_router();
let routes = router.registered_routes();
assert!(routes.contains_key("/"), "Expected UI route to be registered");
}

// **Feature: router-nesting, Property 11: OpenAPI Integration**
//
// For any nested routes with OpenAPI operations, the operations should appear
Expand Down
16 changes: 14 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,25 @@ async fn hello(Path(name): Path<String>) -> Json<Message> {
Json(Message { greeting: format!("Hello, {name}!") })
}

async fn homepage() -> Html<&'static str> {
Html("<h1>Welcome</h1>")
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
RustApi::auto().run("0.0.0.0:8080").await
RustApi::new()
// API routes show up in OpenAPI under /api/*
.route("/api/hello/{name}", get(hello))
// UI/SSR routes are mounted without OpenAPI entries by default
.route_ui("/", get(homepage))
.run("0.0.0.0:8080")
.await
}
```

Visit `http://localhost:8080/docs` for auto-generated Swagger UI.
Visit `http://localhost:8080/docs` for auto-generated Swagger UI. Routes under `/api/*`
are included in OpenAPI by default, while UI routes mounted with `route_ui`/`mount_route_ui`
are excluded unless you register them as API routes.

## Examples

Expand Down
Loading