Skip to main content

pagesift_api/
validate.rs

1//! 请求校验:绕过类参数和引擎做不到的功能一律明确报错(PRD 6.5.1),不悄悄降级。
2
3use crate::engine::Engine;
4use crate::error::ApiError;
5use crate::types::ScrapeOptions;
6
7pub fn check_url(raw: &str) -> Result<(), ApiError> {
8    match url::Url::parse(raw) {
9        Ok(u) if matches!(u.scheme(), "http" | "https") && u.host().is_some() => Ok(()),
10        Ok(_) => Err(ApiError::invalid(format!(
11            "only http(s) URLs are supported: {raw}"
12        ))),
13        Err(e) => Err(ApiError::invalid(format!("invalid URL {raw:?}: {e}"))),
14    }
15}
16
17pub fn check_scrape_options(opts: &ScrapeOptions, engine: &dyn Engine) -> Result<(), ApiError> {
18    if opts.discover_scripts
19        && (opts.engine.as_deref() != Some("http")
20            || !opts.wants("links")
21            || opts.egress.is_some()
22            || !opts.headers.is_empty()
23            || opts.if_none_match.is_some()
24            || opts.if_modified_since.is_some())
25    {
26        return Err(ApiError::unsupported(
27            "discoverScripts requires HTTP engine, links, and no custom headers/egress/conditional request",
28        ));
29    }
30    if let Some(p) = opts.proxy.as_deref() {
31        if !matches!(p, "basic" | "auto") {
32            let valid_url = url::Url::parse(p)
33                .ok()
34                .is_some_and(|u| matches!(u.scheme(), "http" | "https" | "socks5" | "socks5h"));
35            if !valid_url {
36                return Err(ApiError::unsupported(format!(
37                    "proxy {p:?} is not supported: use \"basic\", \"auto\", or a valid http/https/socks5 URL"
38                )));
39            }
40        }
41    }
42    if let Some(e) = opts.engine.as_deref()
43        && !matches!(e, "http" | "browser" | "auto")
44    {
45        return Err(ApiError::invalid(format!(
46            "engine must be http, browser or auto, got {e:?}"
47        )));
48    }
49    if let Some(c) = &opts.chunking {
50        const KINDS: [&str; 8] = [
51            "recursive",
52            "headings",
53            "paragraphs",
54            "sentences",
55            "fixed_tokens",
56            "sliding_window",
57            "overlapping",
58            "regex",
59        ];
60        let kind = c.get("type").and_then(|t| t.as_str());
61        if !kind.is_some_and(|k| KINDS.contains(&k)) {
62            return Err(ApiError::invalid(format!(
63                "chunking.type must be one of {}",
64                KINDS.join(", ")
65            )));
66        }
67    }
68    let supported = engine.supported_formats();
69    if let Some(f) = opts
70        .formats
71        .iter()
72        .find(|f| !supported.contains(&f.kind.as_str()))
73    {
74        return Err(ApiError::unsupported(format!(
75            "format {:?} is not supported by this build (supported: {})",
76            f.kind,
77            supported.join(", ")
78        )));
79    }
80    if !opts.actions.is_empty() && !engine.supports_actions() {
81        return Err(ApiError::unsupported(
82            "actions need browser rendering, which is not available in this build",
83        ));
84    }
85    if let Some(t) = opts.timeout {
86        if t < 1000 {
87            return Err(ApiError::invalid("timeout must be at least 1000 ms"));
88        }
89        if opts.wait_for > t / 2 {
90            return Err(ApiError::invalid("waitFor must not exceed half of timeout"));
91        }
92    }
93    if opts.wait_for > 60_000 {
94        return Err(ApiError::invalid("waitFor must not exceed 60000 ms"));
95    }
96    Ok(())
97}