Skip to main content

phrona_api/
tools.rs

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/// GET /v1/extract?url=...&max_chars=...&query=... - readable-text
14/// extraction of a page (the same feature as `ms extract`). Auth is
15/// header-only: query-string credentials are rejected.
16#[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/// POST /v1/extract - same feature, credentials via headers or body.
26#[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/// GET /v1/test?query=...&category=...&max_results=... - availability probe
38/// across every category (the same feature as `ms test`).
39#[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}