use std::sync::Arc;
use axum::Json;
use axum::extract::State;
use axum::response::IntoResponse;
use serde::Deserialize;
use serde_json::{Value, json};
use phrona::models::Category;
use crate::{AppError, AppResult, AppState, HeaderAuth, JsonBody, JsonQuery};
#[derive(Deserialize)]
pub struct ExtractGetParams {
url: String,
#[serde(default)]
max_chars: Option<usize>,
#[serde(default)]
query: Option<String>,
}
#[derive(Deserialize)]
pub struct ExtractPostParams {
url: String,
#[serde(default)]
max_chars: Option<usize>,
#[serde(default)]
query: Option<String>,
#[serde(default)]
api_key: Option<String>,
}
#[derive(Deserialize)]
pub struct TestParams {
#[serde(default)]
query: Option<String>,
#[serde(default)]
category: Option<String>,
#[serde(default)]
max_results: Option<usize>,
}
pub async fn extract_get(
State(state): State<Arc<AppState>>,
auth: HeaderAuth,
JsonQuery(p): JsonQuery<ExtractGetParams>,
) -> AppResult<impl IntoResponse> {
if !state.authorized(auth.key()) {
return Err(AppError::unauthorized());
}
run_extract(&state, &p.url, p.max_chars, p.query.as_deref()).await
}
pub async fn extract_post(
State(state): State<Arc<AppState>>,
headers: axum::http::HeaderMap,
JsonBody(p): JsonBody<ExtractPostParams>,
) -> AppResult<impl IntoResponse> {
if !state.authorized(crate::auth_key(&headers, p.api_key.as_deref()).as_deref()) {
return Err(AppError::unauthorized());
}
run_extract(&state, &p.url, p.max_chars, p.query.as_deref()).await
}
async fn run_extract(
state: &AppState,
url: &str,
max_chars: Option<usize>,
query: Option<&str>,
) -> AppResult<Json<phrona::ExtractedPage>> {
let max_chars = max_chars.unwrap_or(5000).clamp(1, 100_000);
let page = phrona::extract(state.client.http(), url, max_chars, query).await?;
Ok(Json(page))
}
pub async fn test(
State(state): State<Arc<AppState>>,
auth: HeaderAuth,
JsonQuery(p): JsonQuery<TestParams>,
) -> AppResult<Json<Value>> {
if !state.authorized(auth.key()) {
return Err(AppError::unauthorized());
}
let cats: Vec<Category> = match p.category.as_deref() {
Some(c) => vec![c.parse::<Category>().map_err(|_| {
AppError::bad_request(
"invalid category, expected one of: web, images, news, videos, books",
)
})?],
None => Category::ALL.to_vec(),
};
let query = p.query.unwrap_or_else(|| "rust programming".to_string());
let max_results = p.max_results.unwrap_or(5).clamp(1, 10);
let mut out = Vec::new();
for cat in cats {
let mut opts = phrona::SearchOptions::new(query.clone());
opts.category = cat;
opts.max_results = max_results;
match state.client.search(opts).await {
Ok(resp) => out.push(json!({
"category": cat.as_str(),
"total": resp.total,
"elapsed_ms": resp.elapsed_ms,
"answer": resp.answer,
"engines": resp.engines,
})),
Err(e) => out.push(json!({
"category": cat.as_str(),
"total": 0,
"elapsed_ms": 0,
"answer": null,
"engines": [],
"error": e.to_string(),
})),
}
}
Ok(Json(Value::Array(out)))
}