zino_http/response/
webhook.rs

1use crate::helper;
2use http::{
3    Method,
4    header::{HeaderMap, HeaderName},
5};
6use serde::{Serialize, de::DeserializeOwned};
7use serde_json::value::RawValue;
8use toml::Table;
9use url::Url;
10use zino_core::{
11    JsonValue, Map,
12    application::Agent,
13    bail,
14    error::Error,
15    extension::{HeaderMapExt, JsonObjectExt, JsonValueExt, TomlTableExt, TomlValueExt},
16    trace::TraceContext,
17};
18
19/// User-defined HTTP callbacks.
20pub struct WebHook {
21    /// Webhook name.
22    name: String,
23    /// HTTP request method (VERB).
24    method: Method,
25    /// Base URL.
26    base_url: Url,
27    /// HTTP request query.
28    query: Map,
29    /// HTTP request headers.
30    headers: Map,
31    /// Optional request body.
32    body: Option<Box<RawValue>>,
33    /// Optional request params.
34    params: Option<Map>,
35}
36
37impl WebHook {
38    /// Attempts to construct a new instance from the config.
39    pub fn try_new(config: &Table) -> Result<Self, Error> {
40        let name = config.get_str("name").unwrap_or("webhook");
41        let method = if let Some(method) = config.get_str("method") {
42            method.parse()?
43        } else {
44            Method::GET
45        };
46        let mut base_url = if let Some(base_url) = config.get_str("base-url") {
47            base_url.parse::<Url>()?
48        } else {
49            bail!("base URL should be specified");
50        };
51        if let Some(query) = config.get_table("query") {
52            let query = serde_qs::to_string(query)?;
53            base_url.set_query(Some(&query));
54        }
55
56        let headers = config
57            .get("headers")
58            .and_then(|v| v.to_json_value().into_map_opt())
59            .unwrap_or_default();
60        let body = if let Some(body) = config.get_table("body") {
61            Some(serde_json::value::to_raw_value(body)?)
62        } else {
63            None
64        };
65        let params = config
66            .get("params")
67            .and_then(|v| v.to_json_value().into_map_opt());
68        Ok(Self {
69            name: name.to_owned(),
70            method,
71            base_url,
72            query: Map::new(),
73            headers,
74            body,
75            params,
76        })
77    }
78
79    /// Adds a key/value pair for the request query.
80    #[inline]
81    pub fn query(mut self, key: &str, value: impl Into<JsonValue>) -> Self {
82        let value = value.into();
83        if !value.is_null() {
84            self.query.upsert(key, value);
85        }
86        self
87    }
88
89    /// Adds a parameter for the request query.
90    #[inline]
91    pub fn query_param(mut self, key: &str, param: Option<&str>) -> Self {
92        if let Some(param) = param {
93            self.query.upsert(key, ["${", param, "}"].concat());
94        } else {
95            self.query.upsert(key, ["${", key, "}"].concat());
96        }
97        self
98    }
99
100    /// Builds the request query.
101    pub fn build_query(mut self) -> Result<Self, Error> {
102        if !self.query.is_empty() {
103            let query = serde_qs::to_string(&self.query)?;
104            self.base_url.set_query(Some(&query));
105            self.query.clear();
106        }
107        Ok(self)
108    }
109
110    /// Adds a key/value pair for the request headers.
111    #[inline]
112    pub fn header(mut self, key: &str, value: impl Into<JsonValue>) -> Self {
113        self.headers.upsert(key, value);
114        self
115    }
116
117    /// Sets the request query.
118    #[inline]
119    pub fn set_query<T: Serialize>(&mut self, query: &T) {
120        if let Ok(query) = serde_qs::to_string(query) {
121            self.base_url.set_query(Some(&query));
122        }
123    }
124
125    /// Sets the request body.
126    #[inline]
127    pub fn set_body<T: Serialize>(&mut self, body: &T) {
128        self.body = serde_json::value::to_raw_value(body).ok();
129    }
130
131    /// Sets the request params.
132    #[inline]
133    pub fn set_params(&mut self, params: impl Into<JsonValue>) {
134        self.params = params.into().into_map_opt();
135    }
136
137    /// Returns the webhook name.
138    #[inline]
139    pub fn name(&self) -> &str {
140        &self.name
141    }
142
143    /// Triggers the webhook and deserializes the response body via JSON.
144    pub async fn trigger<T: DeserializeOwned>(&self) -> Result<T, Error> {
145        let params = self.params.as_ref();
146        let mut url = self.base_url.clone();
147        if !self.query.is_empty() {
148            let query = serde_qs::to_string(&self.query)?;
149            url.set_query(Some(&query));
150        }
151
152        let url = percent_encoding::percent_decode_str(url.as_str()).decode_utf8()?;
153        let resource = helper::format_query(&url, params);
154        let mut options = Map::from_entry("method", self.method.as_str());
155        if let Some(body) = self.body.as_deref().map(|v| v.get()) {
156            options.upsert("body", helper::format_query(body, params));
157        }
158
159        let mut headers = HeaderMap::new();
160        for (key, value) in self.headers.iter() {
161            if let Ok(header_name) = HeaderName::try_from(key) {
162                let header_value = value
163                    .as_str()
164                    .and_then(|s| helper::format_query(s, params).parse().ok());
165                if let Some(header_value) = header_value {
166                    headers.insert(header_name, header_value);
167                }
168            }
169        }
170
171        let mut trace_context = TraceContext::new();
172        trace_context.record_trace_state();
173
174        let response = Agent::request_builder(resource.as_ref(), Some(&options))?
175            .headers(headers)
176            .header("traceparent", trace_context.traceparent())
177            .header("tracestate", trace_context.tracestate())
178            .send()
179            .await?
180            .error_for_status()?;
181        let data = if response.headers().has_json_content_type() {
182            response.json().await?
183        } else {
184            let text = response.text().await?;
185            serde_json::from_str(&text)?
186        };
187        Ok(data)
188    }
189}