use rovo::aide::openapi::OpenApi;
use rovo::extract::{Path, State};
use rovo::response::Json;
use rovo::routing::get;
use rovo::schemars::JsonSchema;
use rovo::{rovo, Router};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone)]
struct AppState;
#[derive(Serialize, JsonSchema, Default)]
struct Item {
id: String,
name: String,
}
#[rovo]
async fn get_user_by_u64(Path(id): Path<u64>) -> Json<String> {
Json(format!("User {id}"))
}
#[test]
fn test_single_u64_path_parameter() {
use rovo::aide::openapi::Operation;
use rovo::aide::transform::TransformOperation;
let mut op = Operation::default();
let transform = TransformOperation::new(&mut op);
let _ = get_user_by_u64::__docs(transform);
let mut api = OpenApi::default();
api.info.title = "Test API".to_string();
let app = Router::new()
.route("/users/{id}", get(get_user_by_u64))
.with_oas(api)
.with_state(())
.finish();
let spec = extract_openapi_from_router(app);
let paths = &spec.paths.as_ref().unwrap().paths;
let user_path = get_path_item(paths.get("/users/{id}").unwrap());
let get_op = user_path.get.as_ref().unwrap();
let id_param = find_path_parameter(&get_op.parameters, "id");
assert!(id_param.is_some(), "Should have 'id' path parameter");
let param_data = get_parameter_data(id_param.unwrap());
let desc = param_data.description.as_deref().unwrap_or("");
assert_eq!(
desc, "The user\\'s numeric identifier",
"Should have description from doc comment, got: {:?}",
desc
);
}
#[rovo]
async fn get_user_by_string(Path(username): Path<String>) -> Json<String> {
Json(format!("User: {username}"))
}
#[test]
fn test_single_string_path_parameter() {
let mut api = OpenApi::default();
api.info.title = "Test API".to_string();
let app = Router::new()
.route("/users/{username}", get(get_user_by_string))
.with_oas(api)
.with_state(())
.finish();
let spec = extract_openapi_from_router(app);
let paths = &spec.paths.as_ref().unwrap().paths;
let user_path = get_path_item(paths.get("/users/{username}").unwrap());
let get_op = user_path.get.as_ref().unwrap();
let username_param = find_path_parameter(&get_op.parameters, "username");
assert!(
username_param.is_some(),
"Should have 'username' path parameter"
);
}
#[rovo]
async fn get_by_uuid(Path(resource_id): Path<Uuid>) -> Json<String> {
Json(format!("Resource: {resource_id}"))
}
#[test]
fn test_uuid_path_parameter() {
let mut api = OpenApi::default();
api.info.title = "Test API".to_string();
let app = Router::new()
.route("/resources/{resource_id}", get(get_by_uuid))
.with_oas(api)
.with_state(())
.finish();
let spec = extract_openapi_from_router(app);
let paths = &spec.paths.as_ref().unwrap().paths;
let resource_path = get_path_item(paths.get("/resources/{resource_id}").unwrap());
let get_op = resource_path.get.as_ref().unwrap();
let uuid_param = find_path_parameter(&get_op.parameters, "resource_id");
assert!(
uuid_param.is_some(),
"Should have 'resource_id' path parameter"
);
}
#[rovo]
async fn get_by_active(Path(active): Path<bool>) -> Json<String> {
Json(format!("Active: {active}"))
}
#[test]
fn test_bool_path_parameter() {
let mut api = OpenApi::default();
api.info.title = "Test API".to_string();
let app = Router::new()
.route("/items/{active}", get(get_by_active))
.with_oas(api)
.with_state(())
.finish();
let spec = extract_openapi_from_router(app);
let paths = &spec.paths.as_ref().unwrap().paths;
let path = get_path_item(paths.get("/items/{active}").unwrap());
let get_op = path.get.as_ref().unwrap();
let active_param = find_path_parameter(&get_op.parameters, "active");
assert!(
active_param.is_some(),
"Should have 'active' path parameter"
);
}
#[rovo]
async fn get_item_in_collection(Path((collection_id, index)): Path<(Uuid, u32)>) -> Json<String> {
Json(format!("Collection {collection_id}, item {index}"))
}
#[test]
fn test_tuple_path_parameters() {
let mut api = OpenApi::default();
api.info.title = "Test API".to_string();
let app = Router::new()
.route(
"/collections/{collection_id}/items/{index}",
get(get_item_in_collection),
)
.with_oas(api)
.with_state(())
.finish();
let spec = extract_openapi_from_router(app);
let paths = &spec.paths.as_ref().unwrap().paths;
let path = get_path_item(
paths
.get("/collections/{collection_id}/items/{index}")
.unwrap(),
);
let get_op = path.get.as_ref().unwrap();
let collection_param = find_path_parameter(&get_op.parameters, "collection_id");
let index_param = find_path_parameter(&get_op.parameters, "index");
assert!(
collection_param.is_some(),
"Should have 'collection_id' parameter"
);
assert!(index_param.is_some(), "Should have 'index' parameter");
let collection_data = get_parameter_data(collection_param.unwrap());
assert_eq!(
collection_data.description.as_deref(),
Some("The collection UUID"),
"Should have collection_id description"
);
let index_data = get_parameter_data(index_param.unwrap());
assert_eq!(
index_data.description.as_deref(),
Some("The item index within the collection"),
"Should have index description"
);
}
#[rovo]
async fn get_item_no_desc(Path(item_id): Path<i64>) -> Json<String> {
Json(format!("Item {item_id}"))
}
#[test]
fn test_path_param_without_description() {
let mut api = OpenApi::default();
api.info.title = "Test API".to_string();
let app = Router::new()
.route("/items/{item_id}", get(get_item_no_desc))
.with_oas(api)
.with_state(())
.finish();
let spec = extract_openapi_from_router(app);
let paths = &spec.paths.as_ref().unwrap().paths;
let path = get_path_item(paths.get("/items/{item_id}").unwrap());
let get_op = path.get.as_ref().unwrap();
let item_param = find_path_parameter(&get_op.parameters, "item_id");
assert!(item_param.is_some(), "Should have 'item_id' path parameter");
let param_data = get_parameter_data(item_param.unwrap());
assert!(
param_data.description.is_none() || param_data.description.as_deref() == Some(""),
"Should have no description"
);
}
#[derive(Deserialize, JsonSchema)]
struct UserId {
id: u64,
}
#[rovo]
async fn get_user_struct(Path(UserId { id }): Path<UserId>) -> Json<String> {
Json(format!("User {id}"))
}
#[test]
fn test_backwards_compat_struct_path() {
let mut api = OpenApi::default();
api.info.title = "Test API".to_string();
let app = Router::new()
.route("/users/{id}", get(get_user_struct))
.with_oas(api)
.with_state(())
.finish();
let spec = extract_openapi_from_router(app);
let paths = &spec.paths.as_ref().unwrap().paths;
let user_path = get_path_item(paths.get("/users/{id}").unwrap());
let get_op = user_path.get.as_ref().unwrap();
let id_param = find_path_parameter(&get_op.parameters, "id");
assert!(
id_param.is_some(),
"Should have 'id' path parameter from struct"
);
}
#[rovo]
async fn get_item_with_state(State(_state): State<AppState>, Path(id): Path<String>) -> Json<Item> {
Json(Item {
id,
name: "Test".to_string(),
})
}
#[test]
fn test_primitive_path_with_state() {
let state = AppState;
let mut api = OpenApi::default();
api.info.title = "Test API".to_string();
let app = Router::new()
.route("/items/{id}", get(get_item_with_state))
.with_oas(api)
.with_state(state)
.finish();
let spec = extract_openapi_from_router(app);
let paths = &spec.paths.as_ref().unwrap().paths;
let path = get_path_item(paths.get("/items/{id}").unwrap());
let get_op = path.get.as_ref().unwrap();
let id_param = find_path_parameter(&get_op.parameters, "id");
assert!(id_param.is_some(), "Should have 'id' path parameter");
let param_data = get_parameter_data(id_param.unwrap());
assert_eq!(
param_data.description.as_deref(),
Some("The item identifier"),
"Should have description"
);
}
fn get_path_item(
path: &rovo::aide::openapi::ReferenceOr<rovo::aide::openapi::PathItem>,
) -> &rovo::aide::openapi::PathItem {
match path {
rovo::aide::openapi::ReferenceOr::Item(item) => item,
_ => panic!("Expected PathItem, got Reference"),
}
}
fn find_path_parameter<'a>(
parameters: &'a [rovo::aide::openapi::ReferenceOr<rovo::aide::openapi::Parameter>],
name: &str,
) -> Option<&'a rovo::aide::openapi::Parameter> {
parameters.iter().find_map(|p| {
if let rovo::aide::openapi::ReferenceOr::Item(param) = p {
if let rovo::aide::openapi::Parameter::Path { parameter_data, .. } = param {
if parameter_data.name == name {
return Some(param);
}
}
}
None
})
}
fn get_parameter_data(
param: &rovo::aide::openapi::Parameter,
) -> &rovo::aide::openapi::ParameterData {
match param {
rovo::aide::openapi::Parameter::Path { parameter_data, .. } => parameter_data,
rovo::aide::openapi::Parameter::Query { parameter_data, .. } => parameter_data,
rovo::aide::openapi::Parameter::Header { parameter_data, .. } => parameter_data,
rovo::aide::openapi::Parameter::Cookie { parameter_data, .. } => parameter_data,
}
}
fn extract_openapi_from_router(app: ::axum::Router) -> OpenApi {
use axum::body::Body;
use axum::http::Request;
use tower::util::ServiceExt;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let response = app
.oneshot(
Request::builder()
.uri("/api.json")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&body).unwrap()
})
}