use reqwest::Url;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HttpFetchArgs {
pub url: String,
pub method: Option<String>,
pub text_format: Option<String>,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HttpRequestArgs {
pub url: String,
pub method: String,
pub json_body: Option<serde_json::Value>,
pub text_format: Option<String>,
}
pub const ABS_MAX_BODY_BYTES: usize = 4 * 1024 * 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum HttpBodyTextFormat {
#[default]
Raw,
HtmlText,
}
pub(super) fn parse_text_format_optional(raw: Option<&str>) -> Result<HttpBodyTextFormat, String> {
let raw = raw
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or("raw");
let n = raw.to_ascii_lowercase().replace('-', "_");
match n.as_str() {
"raw" => Ok(HttpBodyTextFormat::Raw),
"html_text" | "htmltext" | "text" => Ok(HttpBodyTextFormat::HtmlText),
_ => Err(format!(
"text_format 仅支持 raw(默认)或 html_text(收到 {:?})",
raw
)),
}
}
const MAX_REQUEST_JSON_BODY_BYTES: usize = 256 * 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FetchMethod {
Get,
Head,
}
impl FetchMethod {
pub fn as_str(self) -> &'static str {
match self {
FetchMethod::Get => "GET",
FetchMethod::Head => "HEAD",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequestMethod {
Post,
Put,
Patch,
Delete,
}
impl RequestMethod {
pub fn as_str(self) -> &'static str {
match self {
RequestMethod::Post => "POST",
RequestMethod::Put => "PUT",
RequestMethod::Patch => "PATCH",
RequestMethod::Delete => "DELETE",
}
}
pub fn into_reqwest(self) -> reqwest::Method {
match self {
RequestMethod::Post => reqwest::Method::POST,
RequestMethod::Put => reqwest::Method::PUT,
RequestMethod::Patch => reqwest::Method::PATCH,
RequestMethod::Delete => reqwest::Method::DELETE,
}
}
}
pub fn parse_http_fetch_args(
args_json: &str,
) -> Result<(Url, FetchMethod, HttpBodyTextFormat), String> {
let args: HttpFetchArgs =
serde_json::from_str(args_json).map_err(|e| format!("参数 JSON 无效: {e}"))?;
let u = args.url.trim();
if u.is_empty() {
return Err("缺少 url".to_string());
}
let url = Url::parse(u).map_err(|e| format!("URL 解析失败: {}", e))?;
let scheme = url.scheme();
if scheme != "http" && scheme != "https" {
return Err(format!("仅允许 http/https 方案,当前为 {}", scheme));
}
let method_upper = args
.method
.as_deref()
.map(|s| s.trim().to_ascii_uppercase())
.filter(|s| !s.is_empty());
let method = match method_upper.as_deref() {
None | Some("GET") => FetchMethod::Get,
Some("HEAD") => FetchMethod::Head,
Some(other) => {
return Err(format!("method 仅支持 GET 或 HEAD(收到 {:?})", other));
}
};
let text_format = parse_text_format_optional(args.text_format.as_deref())?;
Ok((url, method, text_format))
}
pub fn parse_http_request_args(
args_json: &str,
) -> Result<
(
Url,
RequestMethod,
Option<serde_json::Value>,
HttpBodyTextFormat,
),
String,
> {
let args: HttpRequestArgs =
serde_json::from_str(args_json).map_err(|e| format!("参数 JSON 无效: {e}"))?;
let u = args.url.trim();
if u.is_empty() {
return Err("缺少 url".to_string());
}
let url = Url::parse(u).map_err(|e| format!("URL 解析失败: {}", e))?;
let scheme = url.scheme();
if scheme != "http" && scheme != "https" {
return Err(format!("仅允许 http/https 方案,当前为 {}", scheme));
}
let method_raw = args.method.trim().to_ascii_uppercase();
if method_raw.is_empty() {
return Err("缺少 method(POST/PUT/PATCH/DELETE)".to_string());
}
let method = match method_raw.as_str() {
"POST" => RequestMethod::Post,
"PUT" => RequestMethod::Put,
"PATCH" => RequestMethod::Patch,
"DELETE" => RequestMethod::Delete,
_ => {
return Err(format!(
"method 仅支持 POST/PUT/PATCH/DELETE(收到 {:?})",
method_raw
));
}
};
let json_body = args.json_body.clone();
if let Some(body) = json_body.as_ref() {
let body_len = serde_json::to_vec(body)
.map(|b| b.len())
.map_err(|e| format!("json_body 序列化失败: {}", e))?;
if body_len > MAX_REQUEST_JSON_BODY_BYTES {
return Err(format!(
"json_body 过大:{} 字节(上限 {} 字节)",
body_len, MAX_REQUEST_JSON_BODY_BYTES
));
}
}
let text_format = parse_text_format_optional(args.text_format.as_deref())?;
Ok((url, method, json_body, text_format))
}