#[cfg(test)]
use super::MechanicsConfig;
#[cfg(test)]
use super::headers::extract_exposed_response_headers;
#[cfg(test)]
use super::parse_endpoint_call_options;
#[cfg(test)]
use super::{EndpointCallBody, EndpointCallOptions};
use super::{
HttpMethod,
query::{validate_byte_len, validate_min_max_bounds},
retry::EndpointRetryPolicy,
template::UrlTemplateChunk,
};
use boa_engine::{JsData, Trace};
use boa_gc::Finalize;
use reqwest::header::HeaderName;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[cfg(test)]
use std::io::ErrorKind;
#[cfg(test)]
use self::execute::extend_body_with_limit;
mod execute;
mod request;
mod validate;
#[derive(
JsData, Trace, Finalize, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum EndpointBodyType {
#[default]
Json,
Utf8,
Bytes,
}
#[derive(JsData, Trace, Finalize, Serialize, Deserialize, Clone, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct UrlParamSpec {
#[serde(default)]
pub default: Option<String>,
#[serde(default)]
pub min_bytes: Option<usize>,
#[serde(default)]
pub max_bytes: Option<usize>,
}
impl UrlParamSpec {
fn resolve_value(&self, slot: &str, provided: Option<&str>) -> std::io::Result<String> {
validate_min_max_bounds(slot, self.min_bytes, self.max_bytes)?;
let value = match provided {
Some(v) if !v.is_empty() => v,
Some(_) | None => self.default.as_deref().unwrap_or(""),
};
validate_byte_len(slot, value, self.min_bytes, self.max_bytes)?;
Ok(value.to_owned())
}
}
#[derive(
JsData, Trace, Finalize, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum SlottedQueryMode {
#[default]
Required,
RequiredAllowEmpty,
Optional,
OptionalAllowEmpty,
}
#[derive(JsData, Trace, Finalize, Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum QuerySpec {
Const {
key: String,
value: String,
},
Slotted {
key: String,
slot: String,
#[serde(default)]
mode: SlottedQueryMode,
#[serde(default)]
default: Option<String>,
#[serde(default)]
min_bytes: Option<usize>,
#[serde(default)]
max_bytes: Option<usize>,
},
}
#[derive(JsData, Trace, Finalize, Serialize, Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct HttpEndpoint {
method: HttpMethod,
url_template: String,
#[serde(default)]
url_param_specs: HashMap<String, UrlParamSpec>,
#[serde(default)]
query_specs: Vec<QuerySpec>,
#[serde(default)]
headers: HashMap<String, String>,
#[serde(default)]
overridable_request_headers: Vec<String>,
#[serde(default)]
exposed_response_headers: Vec<String>,
#[serde(default)]
request_body_type: Option<EndpointBodyType>,
#[serde(default)]
response_body_type: EndpointBodyType,
#[serde(default)]
response_max_bytes: Option<usize>,
timeout_ms: Option<u64>,
#[serde(default)]
allow_non_2xx_status: bool,
#[unsafe_ignore_trace]
#[serde(default)]
retry_policy: EndpointRetryPolicy,
}
impl HttpEndpoint {
pub(super) const USER_AGENT: &str = concat!(
"Mozilla/5.0 (compatible; mechanics-rs/",
env!("CARGO_PKG_VERSION"),
")"
);
pub fn new(method: HttpMethod, url_template: &str, headers: HashMap<String, String>) -> Self {
Self {
method,
url_template: url_template.to_owned(),
url_param_specs: HashMap::new(),
query_specs: Vec::new(),
headers,
overridable_request_headers: Vec::new(),
exposed_response_headers: Vec::new(),
request_body_type: None,
response_body_type: EndpointBodyType::Json,
response_max_bytes: None,
timeout_ms: None,
allow_non_2xx_status: false,
retry_policy: EndpointRetryPolicy::default(),
}
}
pub fn with_url_param_specs(mut self, url_param_specs: HashMap<String, UrlParamSpec>) -> Self {
self.url_param_specs = url_param_specs;
self
}
pub fn with_query_specs(mut self, query_specs: Vec<QuerySpec>) -> Self {
self.query_specs = query_specs;
self
}
pub fn with_request_body_type(mut self, body_type: EndpointBodyType) -> Self {
self.request_body_type = Some(body_type);
self
}
pub fn with_overridable_request_headers(mut self, headers: Vec<String>) -> Self {
self.overridable_request_headers = headers;
self
}
pub fn with_exposed_response_headers(mut self, headers: Vec<String>) -> Self {
self.exposed_response_headers = headers;
self
}
pub fn with_response_body_type(mut self, body_type: EndpointBodyType) -> Self {
self.response_body_type = body_type;
self
}
pub fn with_response_max_bytes(mut self, response_max_bytes: Option<usize>) -> Self {
self.response_max_bytes = response_max_bytes;
self
}
pub fn with_timeout_ms(mut self, timeout_ms: Option<u64>) -> Self {
self.timeout_ms = timeout_ms;
self
}
pub fn with_allow_non_2xx_status(mut self, allow: bool) -> Self {
self.allow_non_2xx_status = allow;
self
}
pub fn with_retry_policy(mut self, retry_policy: EndpointRetryPolicy) -> Self {
self.retry_policy = retry_policy;
self
}
}
#[derive(Clone, Debug)]
pub(crate) struct PreparedHttpEndpoint {
parsed_url_chunks: Vec<UrlTemplateChunk>,
url_slot_names: Vec<String>,
url_slot_set: HashSet<String>,
allowed_query_slots: HashSet<String>,
allowed_overrides: HashSet<HeaderName>,
exposed_response_allowlist: HashSet<HeaderName>,
}
#[cfg(test)]
#[path = "../tests/mod.rs"]
mod tests;