use std::sync::Arc;
use axum::{
body::Bytes,
extract::{Path, Query, Request, State},
http::{header, HeaderMap, StatusCode},
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use chrono::{DateTime, Utc};
use serde_json::{json, Value};
use crate::artifact::verify_artifact;
use crate::store::Store;
const MAX_BODY: usize = 64 * 1024;
#[derive(Clone)]
pub struct AppState {
pub store: Store,
pub now_fn: Arc<dyn Fn() -> DateTime<Utc> + Send + Sync>,
}
impl AppState {
pub fn new(store: Store) -> Self {
Self { store, now_fn: Arc::new(Utc::now) }
}
pub fn with_now_fn<F>(store: Store, now_fn: F) -> Self
where
F: Fn() -> DateTime<Utc> + Send + Sync + 'static,
{
Self { store, now_fn: Arc::new(now_fn) }
}
}
pub fn create_app(state: AppState) -> Router {
Router::new()
.route("/questions", post(post_question).get(get_questions))
.route("/answers", post(post_answer))
.route("/ratings", post(post_rating))
.route("/artifact/:cid", get(get_artifact))
.route("/feed", get(get_feed))
.with_state(state)
}
async fn post_question(state: State<AppState>, headers: HeaderMap, req: Request) -> Response {
handle_post(state, headers, req, "question").await
}
async fn post_answer(state: State<AppState>, headers: HeaderMap, req: Request) -> Response {
handle_post(state, headers, req, "answer").await
}
async fn post_rating(state: State<AppState>, headers: HeaderMap, req: Request) -> Response {
handle_post(state, headers, req, "rating").await
}
async fn handle_post(
State(state): State<AppState>,
headers: HeaderMap,
req: Request,
expected_kind: &str,
) -> Response {
if let Some(cl) = headers.get(header::CONTENT_LENGTH) {
if let Ok(s) = cl.to_str() {
if let Ok(n) = s.parse::<usize>() {
if n > MAX_BODY {
return (
StatusCode::PAYLOAD_TOO_LARGE,
Json(json!({"error": "body too large"})),
).into_response();
}
}
}
}
let body = match axum::body::to_bytes(req.into_body(), MAX_BODY + 1).await {
Ok(b) => b,
Err(_) => {
return (StatusCode::PAYLOAD_TOO_LARGE, Json(json!({"error": "body too large"})))
.into_response();
}
};
if body.len() > MAX_BODY {
return (StatusCode::PAYLOAD_TOO_LARGE, Json(json!({"error": "body too large"})))
.into_response();
}
if body.is_empty() {
return (StatusCode::BAD_REQUEST, Json(json!({"error": "invalid json"}))).into_response();
}
let parsed: Value = match serde_json::from_slice(&body) {
Ok(v) => v,
Err(_) => {
return (StatusCode::BAD_REQUEST, Json(json!({"error": "invalid json"})))
.into_response();
}
};
ingest(&state, parsed, expected_kind).await
}
async fn ingest(state: &AppState, body: Value, expected_kind: &str) -> Response {
let v = verify_artifact(&body);
if !v.ok {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": format!("verify: {}", v.errors.join("; "))})),
)
.into_response();
}
let kind = body.get("kind").and_then(|x| x.as_str()).unwrap_or("");
if kind != expected_kind {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": format!("kind mismatch: expected {expected_kind}")})),
)
.into_response();
}
let now_ms = (state.now_fn)().timestamp_millis();
let created_at = body.get("created_at").and_then(|v| v.as_str()).unwrap_or("");
let created_ms = match DateTime::parse_from_rfc3339(created_at) {
Ok(d) => d.with_timezone(&Utc).timestamp_millis(),
Err(_) => {
return (StatusCode::BAD_REQUEST, Json(json!({"error": "invalid created_at"})))
.into_response();
}
};
if (now_ms - created_ms).abs() > 24 * 60 * 60 * 1000 {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "created_at outside ±24h window"})),
)
.into_response();
}
match kind {
"answer" => {
let qcid = body.get("question_cid").and_then(|v| v.as_str()).unwrap_or("");
match state.store.has_artifact(qcid) {
Ok(false) => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "question_cid not known locally"})),
).into_response(),
Err(e) => return (
StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})),
).into_response(),
_ => {}
}
}
"rating" => {
let tcid = body.get("target_cid").and_then(|v| v.as_str()).unwrap_or("");
match state.store.has_artifact(tcid) {
Ok(false) => return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "target_cid not known locally"})),
).into_response(),
Err(e) => return (
StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})),
).into_response(),
_ => {}
}
}
_ => {}
}
match state.store.insert_artifact(&body) {
Ok(cid) => (StatusCode::CREATED, Json(json!({"cid": cid}))).into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})),
).into_response(),
}
}
async fn get_artifact(
State(state): State<AppState>,
Path(cid): Path<String>,
) -> Response {
match state.store.get_artifact(&cid) {
Ok(Some(a)) => Json(a).into_response(),
Ok(None) => (StatusCode::NOT_FOUND, Json(json!({"error": "not found"}))).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
}
}
#[derive(serde::Deserialize)]
struct ListQ {
tag: Option<String>,
since: Option<String>,
}
async fn get_questions(
State(state): State<AppState>,
Query(q): Query<ListQ>,
) -> Response {
match state.store.list_questions(q.tag.as_deref(), q.since.as_deref(), 100) {
Ok(rows) => Json(rows).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
}
}
#[derive(serde::Deserialize)]
struct FeedQ {
since: Option<String>,
}
async fn get_feed(
State(state): State<AppState>,
Query(q): Query<FeedQ>,
) -> Response {
let items = match state.store.stream_feed(q.since.as_deref(), 1000) {
Ok(r) => r,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
};
let mut buf = Vec::new();
for item in items {
let line = serde_json::to_string(&item).unwrap_or_default();
buf.extend_from_slice(line.as_bytes());
buf.push(b'\n');
}
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/x-ndjson; charset=utf-8")],
Bytes::from(buf),
)
.into_response()
}