Skip to main content

origin_asset/
transport.rs

1use reqwest::{Client, Method, Response};
2use serde::de::DeserializeOwned;
3use serde_json::Value;
4
5use crate::error::{OriginError, Result};
6
7/// Shared HTTP transport handling auth, envelope parsing, and error mapping.
8#[derive(Debug, Clone)]
9pub struct HttpTransport {
10    client: Client,
11    base_url: String,
12    api_key: String,
13}
14
15impl HttpTransport {
16    pub fn new(base_url: impl Into<String>, api_key: impl Into<String>) -> Self {
17        Self {
18            client: Client::new(),
19            base_url: base_url.into().trim_end_matches('/').to_string(),
20            api_key: api_key.into(),
21        }
22    }
23
24    /// Use a custom `reqwest::Client` (e.g. with proxy or custom timeout).
25    pub fn with_client(mut self, client: Client) -> Self {
26        self.client = client;
27        self
28    }
29
30    pub fn client(&self) -> &Client {
31        &self.client
32    }
33
34    pub fn base_url(&self) -> &str {
35        &self.base_url
36    }
37
38    /// Send a JSON request and parse the envelope response.
39    ///
40    /// Origin services return `{"ok": true, "data": ...}` on success
41    /// and `{"ok": false, "error": ...}` on failure.
42    pub async fn request<T: DeserializeOwned>(
43        &self,
44        method: Method,
45        path: &str,
46        body: Option<&Value>,
47    ) -> Result<T> {
48        let url = format!("{}{}", self.base_url, path);
49
50        let mut builder = self.client.request(method, &url).bearer_auth(&self.api_key);
51
52        if let Some(body) = body {
53            builder = builder.json(body);
54        }
55
56        let response = builder.send().await?;
57        self.parse_response(response).await
58    }
59
60    /// GET convenience.
61    pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
62        self.request(Method::GET, path, None).await
63    }
64
65    /// POST convenience.
66    pub async fn post<T: DeserializeOwned>(&self, path: &str, body: &Value) -> Result<T> {
67        self.request(Method::POST, path, Some(body)).await
68    }
69
70    /// DELETE convenience.
71    pub async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
72        self.request(Method::DELETE, path, None).await
73    }
74
75    /// POST with multipart form data.
76    pub async fn post_multipart<T: DeserializeOwned>(
77        &self,
78        path: &str,
79        form: reqwest::multipart::Form,
80    ) -> Result<T> {
81        let url = format!("{}{}", self.base_url, path);
82
83        let response = self
84            .client
85            .post(&url)
86            .bearer_auth(&self.api_key)
87            .multipart(form)
88            .send()
89            .await?;
90
91        self.parse_response(response).await
92    }
93
94    /// Parse the HTTP response, handling the `{"ok", "data"|"error"}` envelope.
95    async fn parse_response<T: DeserializeOwned>(&self, response: Response) -> Result<T> {
96        let status = response.status().as_u16();
97        let text = response.text().await?;
98
99        if text.is_empty() {
100            return Err(OriginError::api(status, "empty response body"));
101        }
102
103        let payload: Value = serde_json::from_str(&text).map_err(|_| {
104            OriginError::api(status, format!("invalid JSON: {}", truncate(&text, 256)))
105        })?;
106
107        // Check for envelope format: {"ok": bool, "data": ..., "error": ...}
108        if let Some(ok) = payload.get("ok").and_then(|v| v.as_bool()) {
109            if ok {
110                let data = payload.get("data").cloned().unwrap_or(Value::Null);
111                let result = serde_json::from_value(data)?;
112                return Ok(result);
113            }
114
115            let command = payload
116                .get("command")
117                .and_then(|v| v.as_str())
118                .map(String::from);
119            let error = payload.get("error").cloned().unwrap_or(Value::Null);
120            let (code, message) = parse_error_payload(&error);
121            return Err(OriginError::api_full(status, code, message, command));
122        }
123
124        if status >= 400 {
125            return Err(OriginError::api(status, truncate(&text, 512).to_string()));
126        }
127
128        let result = serde_json::from_value(payload)?;
129        Ok(result)
130    }
131}
132
133fn parse_error_payload(error: &Value) -> (Option<String>, String) {
134    match error {
135        Value::String(s) => (None, s.clone()),
136        Value::Object(map) => {
137            let code = map.get("code").and_then(|v| v.as_str()).map(String::from);
138            let message = map
139                .get("message")
140                .and_then(|v| v.as_str())
141                .unwrap_or("unknown error")
142                .to_string();
143            (code, message)
144        }
145        _ => (None, error.to_string()),
146    }
147}
148
149fn truncate(s: &str, max: usize) -> &str {
150    if s.len() <= max {
151        s
152    } else {
153        &s[..max]
154    }
155}