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