Skip to main content

supabase_client_functions/
types.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6
7use crate::error::FunctionsError;
8
9/// HTTP methods supported for Edge Function invocation.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum HttpMethod {
12    Get,
13    Post,
14    Put,
15    Patch,
16    Delete,
17    Options,
18    Head,
19}
20
21impl fmt::Display for HttpMethod {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Self::Get => write!(f, "GET"),
25            Self::Post => write!(f, "POST"),
26            Self::Put => write!(f, "PUT"),
27            Self::Patch => write!(f, "PATCH"),
28            Self::Delete => write!(f, "DELETE"),
29            Self::Options => write!(f, "OPTIONS"),
30            Self::Head => write!(f, "HEAD"),
31        }
32    }
33}
34
35/// Supabase Edge Function deployment regions.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum FunctionRegion {
38    UsEast1,
39    UsWest1,
40    UsCentral1,
41    EuWest1,
42    EuWest2,
43    EuWest3,
44    EuCentral1,
45    EuCentral2,
46    ApSoutheast1,
47    ApSoutheast2,
48    ApNortheast1,
49    ApNortheast2,
50    ApSouth1,
51    SaEast1,
52    CaCentral1,
53    MeSouth1,
54    AfSouth1,
55    Any,
56    Custom(String),
57}
58
59impl fmt::Display for FunctionRegion {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::UsEast1 => write!(f, "us-east-1"),
63            Self::UsWest1 => write!(f, "us-west-1"),
64            Self::UsCentral1 => write!(f, "us-central-1"),
65            Self::EuWest1 => write!(f, "eu-west-1"),
66            Self::EuWest2 => write!(f, "eu-west-2"),
67            Self::EuWest3 => write!(f, "eu-west-3"),
68            Self::EuCentral1 => write!(f, "eu-central-1"),
69            Self::EuCentral2 => write!(f, "eu-central-2"),
70            Self::ApSoutheast1 => write!(f, "ap-southeast-1"),
71            Self::ApSoutheast2 => write!(f, "ap-southeast-2"),
72            Self::ApNortheast1 => write!(f, "ap-northeast-1"),
73            Self::ApNortheast2 => write!(f, "ap-northeast-2"),
74            Self::ApSouth1 => write!(f, "ap-south-1"),
75            Self::SaEast1 => write!(f, "sa-east-1"),
76            Self::CaCentral1 => write!(f, "ca-central-1"),
77            Self::MeSouth1 => write!(f, "me-south-1"),
78            Self::AfSouth1 => write!(f, "af-south-1"),
79            Self::Any => write!(f, "any"),
80            Self::Custom(s) => write!(f, "{}", s),
81        }
82    }
83}
84
85/// Body types for Edge Function invocation.
86#[derive(Debug, Clone)]
87pub enum InvokeBody {
88    Json(Value),
89    Bytes(Vec<u8>),
90    Text(String),
91    None,
92}
93
94/// Options for invoking an Edge Function.
95///
96/// # Example
97/// ```
98/// use supabase_client_functions::InvokeOptions;
99/// use serde_json::json;
100///
101/// let opts = InvokeOptions::new()
102///     .body(json!({"name": "World"}))
103///     .header("x-custom", "value");
104/// ```
105#[derive(Debug, Clone)]
106pub struct InvokeOptions {
107    pub(crate) body: InvokeBody,
108    pub(crate) method: HttpMethod,
109    pub(crate) headers: HashMap<String, String>,
110    pub(crate) region: Option<FunctionRegion>,
111    pub(crate) content_type: Option<String>,
112    pub(crate) authorization: Option<String>,
113}
114
115impl Default for InvokeOptions {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121impl InvokeOptions {
122    /// Create new invoke options with defaults (POST, no body).
123    pub fn new() -> Self {
124        Self {
125            body: InvokeBody::None,
126            method: HttpMethod::Post,
127            headers: HashMap::new(),
128            region: None,
129            content_type: None,
130            authorization: None,
131        }
132    }
133
134    /// Set a JSON body (serialized from a `serde_json::Value`).
135    pub fn body(mut self, value: Value) -> Self {
136        self.body = InvokeBody::Json(value);
137        self
138    }
139
140    /// Set a raw binary body.
141    pub fn body_bytes(mut self, bytes: Vec<u8>) -> Self {
142        self.body = InvokeBody::Bytes(bytes);
143        self
144    }
145
146    /// Set a text body.
147    pub fn body_text(mut self, text: impl Into<String>) -> Self {
148        self.body = InvokeBody::Text(text.into());
149        self
150    }
151
152    /// Set the HTTP method.
153    pub fn method(mut self, method: HttpMethod) -> Self {
154        self.method = method;
155        self
156    }
157
158    /// Add a custom header.
159    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
160        self.headers.insert(key.into(), value.into());
161        self
162    }
163
164    /// Add multiple custom headers.
165    pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
166        self.headers.extend(headers);
167        self
168    }
169
170    /// Set the deployment region.
171    pub fn region(mut self, region: FunctionRegion) -> Self {
172        self.region = Some(region);
173        self
174    }
175
176    /// Override the Content-Type header explicitly.
177    pub fn content_type(mut self, ct: impl Into<String>) -> Self {
178        self.content_type = Some(ct.into());
179        self
180    }
181
182    /// Override the Authorization header (e.g., `"Bearer <user-jwt>"`).
183    pub fn authorization(mut self, auth: impl Into<String>) -> Self {
184        self.authorization = Some(auth.into());
185        self
186    }
187}
188
189/// Response from an Edge Function invocation.
190#[derive(Debug, Clone)]
191pub struct FunctionResponse {
192    status: u16,
193    headers: HashMap<String, String>,
194    body: Vec<u8>,
195}
196
197impl FunctionResponse {
198    pub(crate) fn new(status: u16, headers: HashMap<String, String>, body: Vec<u8>) -> Self {
199        Self {
200            status,
201            headers,
202            body,
203        }
204    }
205
206    /// HTTP status code.
207    pub fn status(&self) -> u16 {
208        self.status
209    }
210
211    /// All response headers (keys are lowercased).
212    pub fn headers(&self) -> &HashMap<String, String> {
213        &self.headers
214    }
215
216    /// Get a specific response header (case-insensitive lookup).
217    pub fn header(&self, name: &str) -> Option<&str> {
218        let lower = name.to_lowercase();
219        self.headers.get(&lower).map(|s| s.as_str())
220    }
221
222    /// Deserialize the response body as JSON.
223    pub fn json<T: DeserializeOwned>(&self) -> Result<T, FunctionsError> {
224        serde_json::from_slice(&self.body).map_err(FunctionsError::from)
225    }
226
227    /// Get the response body as a UTF-8 string.
228    pub fn text(&self) -> Result<String, FunctionsError> {
229        String::from_utf8(self.body.clone()).map_err(|e| {
230            FunctionsError::InvalidConfig(format!("Response body is not valid UTF-8: {}", e))
231        })
232    }
233
234    /// Get the raw response body bytes.
235    pub fn bytes(&self) -> &[u8] {
236        &self.body
237    }
238
239    /// Consume the response and return the body bytes.
240    pub fn into_bytes(self) -> Vec<u8> {
241        self.body
242    }
243
244    /// Get the Content-Type header value.
245    pub fn content_type(&self) -> Option<&str> {
246        self.header("content-type")
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn http_method_display() {
256        assert_eq!(HttpMethod::Get.to_string(), "GET");
257        assert_eq!(HttpMethod::Post.to_string(), "POST");
258        assert_eq!(HttpMethod::Put.to_string(), "PUT");
259        assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
260        assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
261        assert_eq!(HttpMethod::Options.to_string(), "OPTIONS");
262        assert_eq!(HttpMethod::Head.to_string(), "HEAD");
263    }
264
265    #[test]
266    fn function_region_display() {
267        assert_eq!(FunctionRegion::UsEast1.to_string(), "us-east-1");
268        assert_eq!(FunctionRegion::EuWest1.to_string(), "eu-west-1");
269        assert_eq!(FunctionRegion::ApNortheast1.to_string(), "ap-northeast-1");
270        assert_eq!(FunctionRegion::Any.to_string(), "any");
271        assert_eq!(
272            FunctionRegion::Custom("my-region".into()).to_string(),
273            "my-region"
274        );
275    }
276
277    #[test]
278    fn invoke_options_defaults() {
279        let opts = InvokeOptions::new();
280        assert!(matches!(opts.body, InvokeBody::None));
281        assert_eq!(opts.method, HttpMethod::Post);
282        assert!(opts.headers.is_empty());
283        assert!(opts.region.is_none());
284        assert!(opts.content_type.is_none());
285        assert!(opts.authorization.is_none());
286    }
287
288    #[test]
289    fn invoke_options_builder() {
290        let opts = InvokeOptions::new()
291            .body(serde_json::json!({"key": "value"}))
292            .method(HttpMethod::Put)
293            .header("x-custom", "test")
294            .region(FunctionRegion::UsEast1)
295            .content_type("text/plain")
296            .authorization("Bearer token123");
297
298        assert!(matches!(opts.body, InvokeBody::Json(_)));
299        assert_eq!(opts.method, HttpMethod::Put);
300        assert_eq!(opts.headers.get("x-custom"), Some(&"test".to_string()));
301        assert_eq!(opts.region, Some(FunctionRegion::UsEast1));
302        assert_eq!(opts.content_type, Some("text/plain".to_string()));
303        assert_eq!(
304            opts.authorization,
305            Some("Bearer token123".to_string())
306        );
307    }
308
309    #[test]
310    fn invoke_options_body_bytes() {
311        let opts = InvokeOptions::new().body_bytes(vec![1, 2, 3]);
312        assert!(matches!(opts.body, InvokeBody::Bytes(ref b) if b == &[1, 2, 3]));
313    }
314
315    #[test]
316    fn invoke_options_body_text() {
317        let opts = InvokeOptions::new().body_text("hello");
318        assert!(matches!(opts.body, InvokeBody::Text(ref s) if s == "hello"));
319    }
320
321    #[test]
322    fn invoke_options_multiple_headers() {
323        let mut extra = HashMap::new();
324        extra.insert("a".into(), "1".into());
325        extra.insert("b".into(), "2".into());
326        let opts = InvokeOptions::new().header("x", "y").headers(extra);
327        assert_eq!(opts.headers.len(), 3);
328        assert_eq!(opts.headers.get("x"), Some(&"y".to_string()));
329        assert_eq!(opts.headers.get("a"), Some(&"1".to_string()));
330    }
331
332    #[test]
333    fn function_response_json() {
334        let resp = FunctionResponse::new(
335            200,
336            HashMap::new(),
337            br#"{"message":"hello"}"#.to_vec(),
338        );
339        let val: serde_json::Value = resp.json().unwrap();
340        assert_eq!(val["message"], "hello");
341    }
342
343    #[test]
344    fn function_response_text() {
345        let resp = FunctionResponse::new(200, HashMap::new(), b"hello world".to_vec());
346        assert_eq!(resp.text().unwrap(), "hello world");
347    }
348
349    #[test]
350    fn function_response_bytes() {
351        let data = vec![0, 1, 2, 255];
352        let resp = FunctionResponse::new(200, HashMap::new(), data.clone());
353        assert_eq!(resp.bytes(), &data);
354    }
355
356    #[test]
357    fn function_response_header_case_insensitive() {
358        let mut headers = HashMap::new();
359        headers.insert("content-type".into(), "application/json".into());
360        headers.insert("x-custom".into(), "value".into());
361        let resp = FunctionResponse::new(200, headers, vec![]);
362        assert_eq!(resp.header("Content-Type"), Some("application/json"));
363        assert_eq!(resp.header("X-Custom"), Some("value"));
364        assert_eq!(resp.header("missing"), None);
365    }
366
367    #[test]
368    fn function_response_content_type() {
369        let mut headers = HashMap::new();
370        headers.insert("content-type".into(), "text/plain".into());
371        let resp = FunctionResponse::new(200, headers, vec![]);
372        assert_eq!(resp.content_type(), Some("text/plain"));
373    }
374
375    #[test]
376    fn function_response_status() {
377        let resp = FunctionResponse::new(201, HashMap::new(), vec![]);
378        assert_eq!(resp.status(), 201);
379    }
380
381    #[test]
382    fn function_response_into_bytes() {
383        let data = vec![10, 20, 30];
384        let resp = FunctionResponse::new(200, HashMap::new(), data.clone());
385        assert_eq!(resp.into_bytes(), data);
386    }
387}