use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use rusqlite::Connection;
use serde::Deserialize;
use serde_json::json;
use std::sync::{Arc, Mutex};
use crate::db::{self, MemoryInput};
#[derive(Clone)]
pub struct AppState {
pub conn: Arc<Mutex<Connection>>,
}
impl AppState {
pub fn new(conn: Connection) -> Self {
Self {
conn: Arc::new(Mutex::new(conn)),
}
}
}
pub fn router(state: AppState) -> Router {
Router::new()
.route("/healthz", get(healthz))
.route("/memories", post(create_memory).get(list_memories))
.route("/memories/:id", get(get_memory).delete(delete_memory))
.route("/search", get(search_memories))
.with_state(state)
}
async fn healthz() -> impl IntoResponse {
Json(json!({
"status": "ok",
"service": "asurada",
"version": env!("CARGO_PKG_VERSION"),
}))
}
async fn create_memory(
State(state): State<AppState>,
Json(input): Json<MemoryInput>,
) -> Result<impl IntoResponse, ApiError> {
let conn = state.conn.lock().map_err(|e| ApiError::internal(e.to_string()))?;
let m = db::memory::insert(&conn, input).map_err(ApiError::from)?;
Ok((StatusCode::CREATED, Json(m)))
}
#[derive(Deserialize)]
struct ListQ {
user_id: String,
#[serde(default)]
scope: Option<String>,
#[serde(default = "default_limit")]
limit: usize,
}
async fn list_memories(
State(state): State<AppState>,
Query(q): Query<ListQ>,
) -> Result<impl IntoResponse, ApiError> {
let conn = state.conn.lock().map_err(|e| ApiError::internal(e.to_string()))?;
let rows = db::memory::list(&conn, &q.user_id, q.scope.as_deref(), q.limit)
.map_err(ApiError::from)?;
Ok(Json(rows))
}
#[derive(Deserialize)]
struct GetQ {
user_id: String,
}
async fn get_memory(
State(state): State<AppState>,
Path(id): Path<String>,
Query(q): Query<GetQ>,
) -> Result<impl IntoResponse, ApiError> {
let conn = state.conn.lock().map_err(|e| ApiError::internal(e.to_string()))?;
let row = db::memory::get(&conn, &q.user_id, &id).map_err(ApiError::from)?;
match row {
Some(m) => Ok(Json(m)),
None => Err(ApiError {
status: StatusCode::NOT_FOUND,
message: "memory not found".into(),
}),
}
}
async fn delete_memory(
State(state): State<AppState>,
Path(id): Path<String>,
Query(q): Query<GetQ>,
) -> Result<impl IntoResponse, ApiError> {
let conn = state.conn.lock().map_err(|e| ApiError::internal(e.to_string()))?;
let ok = db::memory::soft_delete(&conn, &q.user_id, &id).map_err(ApiError::from)?;
if ok {
Ok(StatusCode::NO_CONTENT)
} else {
Err(ApiError {
status: StatusCode::NOT_FOUND,
message: "memory not found".into(),
})
}
}
#[derive(Deserialize)]
struct SearchQ {
user_id: String,
q: String,
#[serde(default = "default_limit")]
limit: usize,
}
async fn search_memories(
State(state): State<AppState>,
Query(q): Query<SearchQ>,
) -> Result<impl IntoResponse, ApiError> {
let conn = state.conn.lock().map_err(|e| ApiError::internal(e.to_string()))?;
let rows = db::memory::search(&conn, &q.user_id, &q.q, q.limit).map_err(ApiError::from)?;
Ok(Json(rows))
}
fn default_limit() -> usize {
20
}
struct ApiError {
status: StatusCode,
message: String,
}
impl ApiError {
fn internal(msg: impl Into<String>) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: msg.into(),
}
}
}
impl From<anyhow::Error> for ApiError {
fn from(e: anyhow::Error) -> Self {
Self::internal(e.to_string())
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
(self.status, Json(json!({ "error": self.message }))).into_response()
}
}