use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::ToolError;
pub const ABI_OUT_OF_BOUNDS: i32 = -1;
pub const ABI_NOTHING_HELD: i32 = -2;
pub const ABI_LENGTH_MISMATCH: i32 = -3;
pub const INLINE_MAX_BYTES: usize = 256 * 1024;
pub const MAX_REDIRECTS: usize = 10;
pub const REQUEST_MAX_BYTES: usize = 8 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Request {
#[serde(default)]
pub method: Method,
#[schemars(
description = "An https URL; the host must be one the function's network setting allows."
)]
pub url: String,
#[serde(default)]
#[schemars(
description = "Request headers by name. A header a configured credential sets for the \
URL's origin is refused; host, content-length and other connection \
headers are ignored."
)]
pub headers: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<RequestBody>,
#[serde(default)]
pub output: ResponseOutput,
#[serde(default = "Request::default_follow_redirects")]
#[schemars(description = format!(
"Follow up to {MAX_REDIRECTS} redirects, each checked like the first request. Stored \
credentials are attached per hop for that hop's origin only. On a redirect to \
another origin, authorization, cookie and proxy-authorization headers you set are \
dropped; other headers are sent as given."
))]
pub follow_redirects: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(
range(min = 1),
description = "Bound on the whole request; the call's own time limit applies regardless."
)]
pub timeout_ms: Option<u64>,
}
impl Request {
pub fn get(url: impl Into<String>) -> Self {
Request {
method: Method::Get,
url: url.into(),
headers: BTreeMap::new(),
body: None,
output: ResponseOutput::Inline,
follow_redirects: Self::default_follow_redirects(),
timeout_ms: None,
}
}
fn default_follow_redirects() -> bool {
true
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "UPPERCASE")]
#[schemars(description = "HTTP method.")]
pub enum Method {
#[default]
Get,
Head,
Post,
Put,
Patch,
Delete,
Options,
}
impl Method {
pub fn as_str(&self) -> &'static str {
match self {
Method::Get => "GET",
Method::Head => "HEAD",
Method::Post => "POST",
Method::Put => "PUT",
Method::Patch => "PATCH",
Method::Delete => "DELETE",
Method::Options => "OPTIONS",
}
}
}
impl std::fmt::Display for Method {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum RequestBody {
#[schemars(description = "Send text, as text/plain unless a content-type header is given.")]
Text { text: String },
#[schemars(
description = "Send this value as JSON, as application/json unless a content-type \
header is given."
)]
Json { json: serde_json::Value },
#[schemars(description = "Stream a volume file as the body.")]
File {
#[schemars(
description = "Absolute volume path under /tmp, or /data when the agent enables it."
)]
path: String,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum ResponseOutput {
#[default]
#[schemars(description = format!(
"Return the body as text in the reply (the default), up to {} KiB of UTF-8.",
INLINE_MAX_BYTES / 1024
))]
Inline,
#[schemars(
description = "Save the body to a volume file, bounded only by the call's write \
budget, and reply with its size. Use this for large or binary responses."
)]
File {
#[schemars(description = "Absolute volume path, overwritten, parents created.")]
path: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Response {
pub status: u16,
#[schemars(description = "Lowercase names; repeated headers joined with a comma.")]
pub headers: BTreeMap<String, String>,
pub body: ResponseBody,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseBody {
Inline { text: String },
File { path: String, bytes: u64 },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HttpError {
pub kind: HttpErrorKind,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HttpErrorKind {
InvalidRequest,
HostNotAllowed,
ForbiddenDestination,
CredentialHeader,
CredentialUnavailable,
TooManyRedirects,
Timeout,
TooLarge,
NotText,
WriteBudget,
BadPath,
Connection,
Abi,
}
impl std::fmt::Display for HttpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for HttpError {}
impl From<HttpError> for ToolError {
fn from(error: HttpError) -> Self {
ToolError::new(error.message)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Answer {
Ok(Response),
Error(HttpError),
}
impl From<Answer> for Result<Response, HttpError> {
fn from(answer: Answer) -> Self {
match answer {
Answer::Ok(response) => Ok(response),
Answer::Error(error) => Err(error),
}
}
}
pub fn request(request: &Request) -> Result<Response, HttpError> {
let abi = |message: String| HttpError {
kind: HttpErrorKind::Abi,
message,
};
let body = serde_json::to_vec(request).map_err(|e| abi(format!("request is not JSON: {e}")))?;
let answer = imports::call(&body).map_err(abi)?;
match serde_json::from_slice::<Answer>(&answer) {
Ok(answer) => answer.into(),
Err(e) => Err(abi(format!("subako_http answered with no answer: {e}"))),
}
}
#[cfg(target_family = "wasm")]
mod imports {
#[link(wasm_import_module = "subako_http")]
unsafe extern "C" {
fn request(ptr: *const u8, len: usize) -> i32;
fn read_response(ptr: *mut u8, len: usize) -> i32;
}
pub(super) fn call(body: &[u8]) -> Result<Vec<u8>, String> {
let failed = |code: i32| format!("subako_http returned {code}");
let len = unsafe { request(body.as_ptr(), body.len()) };
let len = usize::try_from(len).map_err(|_| failed(len))?;
let mut answer = vec![0u8; len];
match unsafe { read_response(answer.as_mut_ptr(), len) } {
0 => Ok(answer),
code => Err(failed(code)),
}
}
}
#[cfg(not(target_family = "wasm"))]
mod imports {
pub(super) fn call(_: &[u8]) -> Result<Vec<u8>, String> {
Err("subako_http exists only inside the function worker".to_string())
}
}