1use std::sync::Arc;
4
5use axum::Json;
6use axum::extract::State;
7use axum::response::IntoResponse;
8use serde::Deserialize;
9use serde_json::{Value, json};
10
11use phrona::models::Category;
12
13use crate::{AppError, AppResult, AppState, HeaderAuth, JsonBody, JsonQuery};
14
15#[derive(Deserialize)]
19pub struct ExtractGetParams {
20 url: String,
21 #[serde(default)]
22 max_chars: Option<usize>,
23 #[serde(default)]
24 query: Option<String>,
25}
26
27#[derive(Deserialize)]
29pub struct ExtractPostParams {
30 url: String,
31 #[serde(default)]
32 max_chars: Option<usize>,
33 #[serde(default)]
34 query: Option<String>,
35 #[serde(default)]
36 api_key: Option<String>,
37}
38
39#[derive(Deserialize)]
42pub struct TestParams {
43 #[serde(default)]
44 query: Option<String>,
45 #[serde(default)]
46 category: Option<String>,
47 #[serde(default)]
48 max_results: Option<usize>,
49}
50
51pub async fn extract_get(
54 State(state): State<Arc<AppState>>,
55 auth: HeaderAuth,
56 JsonQuery(p): JsonQuery<ExtractGetParams>,
57) -> AppResult<impl IntoResponse> {
58 if !state.authorized(auth.key()) {
59 return Err(AppError::unauthorized());
60 }
61 run_extract(&state, &p.url, p.max_chars, p.query.as_deref()).await
62}
63
64pub async fn extract_post(
66 State(state): State<Arc<AppState>>,
67 headers: axum::http::HeaderMap,
68 JsonBody(p): JsonBody<ExtractPostParams>,
69) -> AppResult<impl IntoResponse> {
70 if !state.authorized(crate::auth_key(&headers, p.api_key.as_deref()).as_deref()) {
71 return Err(AppError::unauthorized());
72 }
73 run_extract(&state, &p.url, p.max_chars, p.query.as_deref()).await
74}
75
76async fn run_extract(
77 state: &AppState,
78 url: &str,
79 max_chars: Option<usize>,
80 query: Option<&str>,
81) -> AppResult<Json<phrona::ExtractedPage>> {
82 let max_chars = max_chars.unwrap_or(5000).clamp(1, 100_000);
83 let page = phrona::extract(state.client.http(), url, max_chars, query).await?;
84 Ok(Json(page))
85}
86
87pub async fn test(
90 State(state): State<Arc<AppState>>,
91 auth: HeaderAuth,
92 JsonQuery(p): JsonQuery<TestParams>,
93) -> AppResult<Json<Value>> {
94 if !state.authorized(auth.key()) {
95 return Err(AppError::unauthorized());
96 }
97 let cats: Vec<Category> = match p.category.as_deref() {
98 Some(c) => vec![c.parse::<Category>().map_err(|_| {
99 AppError::bad_request(format!(
100 "invalid category, expected one of: {}",
101 Category::list_str()
102 ))
103 })?],
104 None => Category::ALL.to_vec(),
105 };
106 let query = p.query.unwrap_or_else(|| "rust programming".to_string());
107 let max_results = p.max_results.unwrap_or(5).clamp(1, 10);
108
109 let client = &state.client;
113 let probes = cats.into_iter().map(|cat| {
114 let mut opts = phrona::SearchOptions::new(query.clone());
115 opts.category = cat;
116 opts.max_results = max_results;
117 opts.probe_all = true;
120 async move {
121 match client.search(opts).await {
122 Ok(resp) => json!({
123 "category": cat.as_str(),
124 "total": resp.total,
125 "elapsed_ms": resp.elapsed_ms,
126 "answer": resp.answer,
127 "engines": resp.engines,
128 }),
129 Err(e) => json!({
130 "category": cat.as_str(),
131 "total": 0,
132 "elapsed_ms": 0,
133 "answer": null,
134 "engines": [],
135 "error": e.to_string(),
136 }),
137 }
138 }
139 });
140 let out: Vec<Value> = futures::future::join_all(probes).await;
141 Ok(Json(Value::Array(out)))
142}