use rovo::aide::{axum::IntoApiResponse, openapi::OpenApi};
use rovo::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Json},
};
use rovo::{rovo, schemars::JsonSchema, Router};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tracing::info;
use uuid::Uuid;
#[derive(Clone)]
pub struct AppState {
pub todos: Arc<Mutex<HashMap<Uuid, TodoItem>>>,
}
#[derive(Clone)]
pub struct MetaState {
pub app_name: &'static str,
pub version: &'static str,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct TodoItem {
pub id: Uuid,
pub description: String,
pub complete: bool,
}
impl Default for TodoItem {
fn default() -> Self {
Self {
id: Uuid::nil(),
description: "Sample todo item".into(),
complete: false,
}
}
}
#[derive(Deserialize, JsonSchema)]
pub struct CreateTodoRequest {
pub description: String,
}
#[derive(Deserialize, JsonSchema)]
pub struct UpdateTodoRequest {
pub description: Option<String>,
pub complete: Option<bool>,
}
#[rovo]
async fn get_todo(State(app): State<AppState>, Path(id): Path<Uuid>) -> impl IntoApiResponse {
if let Some(todo) = app.todos.lock().unwrap().get(&id) {
(StatusCode::OK, Json(todo.clone())).into_response()
} else {
StatusCode::NOT_FOUND.into_response()
}
}
#[rovo]
async fn list_todos(State(app): State<AppState>) -> Json<Vec<TodoItem>> {
let todos: Vec<TodoItem> = app.todos.lock().unwrap().values().cloned().collect();
Json(todos)
}
#[rovo]
async fn create_todo(
State(app): State<AppState>,
Json(req): Json<CreateTodoRequest>,
) -> (StatusCode, Json<TodoItem>) {
let todo = TodoItem {
id: Uuid::new_v4(),
description: req.description,
complete: false,
};
app.todos.lock().unwrap().insert(todo.id, todo.clone());
(StatusCode::CREATED, Json(todo))
}
#[rovo]
async fn update_todo(
State(app): State<AppState>,
Path(id): Path<Uuid>,
Json(req): Json<UpdateTodoRequest>,
) -> impl IntoApiResponse {
let mut todos = app.todos.lock().unwrap();
if let Some(todo) = todos.get_mut(&id) {
if let Some(description) = req.description {
todo.description = description;
}
if let Some(complete) = req.complete {
todo.complete = complete;
}
(StatusCode::OK, Json(todo.clone())).into_response()
} else {
StatusCode::NOT_FOUND.into_response()
}
}
#[rovo]
async fn delete_todo(State(app): State<AppState>, Path(id): Path<Uuid>) -> impl IntoApiResponse {
if app.todos.lock().unwrap().remove(&id).is_some() {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
#[derive(Serialize, JsonSchema)]
pub struct HealthResponse {
pub app_name: String,
pub version: String,
pub status: String,
}
#[rovo]
async fn health_check(State(meta): State<MetaState>) -> Json<HealthResponse> {
Json(HealthResponse {
app_name: meta.app_name.to_string(),
version: meta.version.to_string(),
status: "ok".to_string(),
})
}
#[tokio::main]
async fn main() {
use rovo::routing::get;
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,todo_api=debug".into()),
)
.init();
let todo_state = AppState {
todos: Arc::new(Mutex::new(HashMap::new())),
};
let meta_state = MetaState {
app_name: "Todo API",
version: "1.0.0",
};
let mut api = OpenApi::default();
api.info.title = "Todo API Example".to_string();
api.info.description = Some("OpenAPI documentation example using rovo".to_string());
let app = Router::new()
.nest(
"/api",
Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route(
"/todos/{id}",
get(get_todo).patch(update_todo).delete(delete_todo),
)
.with_state(todo_state),
)
.nest(
"/meta",
Router::new()
.route("/health", get(health_check))
.with_state(meta_state),
)
.with_oas(api)
.with_swagger("/")
.finish();
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
let addr = format!("127.0.0.1:{port}");
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
info!("Server started successfully");
info!("Address: http://{addr}");
axum::serve(listener, app).await.unwrap();
}