use std::collections::BTreeMap;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use http::{HeaderMap, HeaderName};
use serde::{Deserialize, Serialize};
use crate::error::Result;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum CallbackBodyType {
#[serde(rename = "application/x-www-form-urlencoded")]
FormUrlEncoded,
#[serde(rename = "application/json")]
Json,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallbackConfiguration {
pub callback_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub callback_host: Option<String>,
pub callback_body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub callback_body_type: Option<CallbackBodyType>,
#[serde(rename = "callbackSNI", skip_serializing_if = "Option::is_none")]
pub callback_sni: Option<bool>,
}
impl CallbackConfiguration {
pub fn new(url: impl Into<String>, body: impl Into<String>) -> Self {
Self {
callback_url: url.into(),
callback_host: None,
callback_body: body.into(),
callback_body_type: None,
callback_sni: None,
}
}
pub fn host(mut self, host: impl Into<String>) -> Self {
self.callback_host = Some(host.into());
self
}
pub fn body_type(mut self, ty: CallbackBodyType) -> Self {
self.callback_body_type = Some(ty);
self
}
pub fn sni(mut self, sni: bool) -> Self {
self.callback_sni = Some(sni);
self
}
pub fn to_base64(&self) -> Result<String> {
let json = serde_json::to_string(self)?;
Ok(BASE64.encode(json.as_bytes()))
}
}
#[derive(Debug, Clone, Default)]
pub struct CallbackVariables(BTreeMap<String, String>);
impl CallbackVariables {
pub fn new() -> Self {
Self::default()
}
pub fn var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
let mut k = key.into();
if !k.starts_with("x:") {
k = format!("x:{k}");
}
self.0.insert(k, value.into());
self
}
pub fn to_base64(&self) -> Result<Option<String>> {
if self.0.is_empty() {
return Ok(None);
}
let json = serde_json::to_string(&self.0)?;
Ok(Some(BASE64.encode(json.as_bytes())))
}
pub fn as_form_fields(&self) -> &BTreeMap<String, String> {
&self.0
}
}
pub fn apply_callback_headers(
mut headers: HeaderMap,
callback: &CallbackConfiguration,
variables: Option<&CallbackVariables>,
) -> Result<HeaderMap> {
let cb_b64 = callback.to_base64()?;
headers.insert(HeaderName::from_static("x-oss-callback"), cb_b64.parse()?);
if let Some(vars) = variables
&& let Some(var_b64) = vars.to_base64()?
{
headers.insert(HeaderName::from_static("x-oss-callback-var"), var_b64.parse()?);
}
Ok(headers)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_callback_to_base64_roundtrip() {
let cfg = CallbackConfiguration::new(
"http://oss-demo.aliyuncs.com:23450",
"bucket=${bucket}&object=${object}",
)
.body_type(CallbackBodyType::FormUrlEncoded)
.sni(false);
let b64 = cfg.to_base64().unwrap();
let json = String::from_utf8(BASE64.decode(&b64).unwrap()).unwrap();
assert!(json.contains("\"callbackUrl\":\"http://oss-demo.aliyuncs.com:23450\""));
assert!(json.contains("\"callbackBodyType\":\"application/x-www-form-urlencoded\""));
assert!(json.contains("\"callbackSNI\":false"));
}
#[test]
fn test_callback_omits_optional_fields() {
let cfg = CallbackConfiguration::new("http://example.com", "bucket=${bucket}");
let b64 = cfg.to_base64().unwrap();
let json = String::from_utf8(BASE64.decode(&b64).unwrap()).unwrap();
assert!(!json.contains("callbackHost"));
assert!(!json.contains("callbackBodyType"));
assert!(!json.contains("callbackSNI"));
}
#[test]
fn test_callback_variables_prefix() {
let vars = CallbackVariables::new()
.var("uid", "12345")
.var("x:order_id", "67890");
let b64 = vars.to_base64().unwrap().unwrap();
let json = String::from_utf8(BASE64.decode(&b64).unwrap()).unwrap();
assert!(json.contains("\"x:uid\":\"12345\""));
assert!(json.contains("\"x:order_id\":\"67890\""));
}
#[test]
fn test_callback_variables_empty_returns_none() {
let vars = CallbackVariables::new();
assert!(vars.to_base64().unwrap().is_none());
}
#[test]
fn test_apply_callback_headers() {
let cfg = CallbackConfiguration::new("http://example.com", "bucket=${bucket}");
let vars = CallbackVariables::new().var("uid", "1");
let headers = apply_callback_headers(HeaderMap::new(), &cfg, Some(&vars)).unwrap();
assert!(headers.contains_key("x-oss-callback"));
assert!(headers.contains_key("x-oss-callback-var"));
}
#[test]
fn test_apply_callback_headers_without_vars() {
let cfg = CallbackConfiguration::new("http://example.com", "bucket=${bucket}");
let headers = apply_callback_headers(HeaderMap::new(), &cfg, None).unwrap();
assert!(headers.contains_key("x-oss-callback"));
assert!(!headers.contains_key("x-oss-callback-var"));
}
}