runbeam_sdk/runbeam_api/
types.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4/// Runbeam API error type
5///
6/// Represents all possible errors that can occur when interacting with
7/// the Runbeam Cloud API or performing related operations.
8#[derive(Debug)]
9pub enum RunbeamError {
10    /// JWT validation failed
11    JwtValidation(String),
12    /// API request failed (network, HTTP, or response parsing error)
13    Api(ApiError),
14    /// Token storage operation failed
15    Storage(crate::storage::StorageError),
16    /// Configuration error
17    Config(String),
18}
19
20impl fmt::Display for RunbeamError {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            RunbeamError::JwtValidation(msg) => write!(f, "JWT validation failed: {}", msg),
24            RunbeamError::Api(err) => write!(f, "API error: {}", err),
25            RunbeamError::Storage(err) => write!(f, "Storage error: {}", err),
26            RunbeamError::Config(msg) => write!(f, "Configuration error: {}", msg),
27        }
28    }
29}
30
31impl std::error::Error for RunbeamError {}
32
33impl From<ApiError> for RunbeamError {
34    fn from(err: ApiError) -> Self {
35        RunbeamError::Api(err)
36    }
37}
38
39impl From<crate::storage::StorageError> for RunbeamError {
40    fn from(err: crate::storage::StorageError) -> Self {
41        RunbeamError::Storage(err)
42    }
43}
44
45impl From<jsonwebtoken::errors::Error> for RunbeamError {
46    fn from(err: jsonwebtoken::errors::Error) -> Self {
47        RunbeamError::JwtValidation(err.to_string())
48    }
49}
50
51/// API-specific errors
52#[derive(Debug)]
53pub enum ApiError {
54    /// Network error (connection, timeout, etc.)
55    Network(String),
56    /// HTTP error with status code
57    Http { status: u16, message: String },
58    /// Failed to parse response
59    Parse(String),
60    /// Request building failed
61    Request(String),
62}
63
64impl fmt::Display for ApiError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            ApiError::Network(msg) => write!(f, "Network error: {}", msg),
68            ApiError::Http { status, message } => {
69                write!(f, "HTTP {} error: {}", status, message)
70            }
71            ApiError::Parse(msg) => write!(f, "Parse error: {}", msg),
72            ApiError::Request(msg) => write!(f, "Request error: {}", msg),
73        }
74    }
75}
76
77impl std::error::Error for ApiError {}
78
79impl From<reqwest::Error> for ApiError {
80    fn from(err: reqwest::Error) -> Self {
81        if err.is_timeout() {
82            ApiError::Network("Request timeout".to_string())
83        } else if err.is_connect() {
84            ApiError::Network(format!("Connection failed: {}", err))
85        } else if let Some(status) = err.status() {
86            ApiError::Http {
87                status: status.as_u16(),
88                message: err.to_string(),
89            }
90        } else {
91            ApiError::Network(err.to_string())
92        }
93    }
94}
95
96/// User information from JWT claims
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct UserInfo {
99    pub id: String,
100    pub email: String,
101    pub name: String,
102}
103
104/// User authentication token (JWT)
105///
106/// This token is used for authenticating user actions with the Runbeam Cloud API.
107/// It has a shorter lifespan than machine tokens and is typically issued after
108/// a user successfully logs in via the browser-based OAuth flow.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct UserToken {
111    /// JWT token for API authentication
112    pub token: String,
113    /// Token expiration timestamp (seconds since Unix epoch)
114    #[serde(default)]
115    pub expires_at: Option<i64>,
116    /// User information from JWT claims
117    #[serde(default)]
118    pub user: Option<UserInfo>,
119}
120
121impl UserToken {
122    /// Create a new user token
123    pub fn new(token: String, expires_at: Option<i64>, user: Option<UserInfo>) -> Self {
124        Self {
125            token,
126            expires_at,
127            user,
128        }
129    }
130}
131
132/// Team information from JWT claims
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct TeamInfo {
135    pub id: String,
136    pub name: String,
137}
138
139/// Gateway information returned from authorize endpoint
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct GatewayInfo {
142    pub id: String,
143    pub code: String,
144    pub name: String,
145    #[serde(default)]
146    pub authorized_by: Option<AuthorizedBy>,
147}
148
149/// User who authorized the gateway
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct AuthorizedBy {
152    pub id: String,
153    pub name: String,
154    pub email: String,
155}
156
157/// Response from Runbeam Cloud authorize endpoint
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct AuthorizeResponse {
160    pub machine_token: String,
161    pub expires_in: f64,
162    pub expires_at: String,
163    pub gateway: GatewayInfo,
164    #[serde(default)]
165    pub abilities: Vec<String>,
166}
167
168/// Request payload for storing/updating Harmony configuration
169///
170/// This is used by the `harmony.update` endpoint to send TOML configuration
171/// from Harmony instances back to Runbeam Cloud for storage as database models.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct StoreConfigRequest {
174    /// Type of configuration being stored ("gateway", "pipeline", or "transform")
175    #[serde(rename = "type")]
176    pub config_type: String,
177    /// Optional ID for updating existing resources (omitted for creates)
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub id: Option<String>,
180    /// TOML configuration content
181    pub config: String,
182}
183
184/// Response from storing/updating Harmony configuration
185///
186/// The API returns UpdateSuccessResource format.
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct StoreConfigResponse {
189    /// Success flag
190    pub success: bool,
191    /// Success message
192    pub message: String,
193    /// Response data with model and change info
194    pub data: StoreConfigResponseData,
195}
196
197/// Data section of store config response
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct StoreConfigResponseData {
200    /// Model information
201    pub model: StoreConfigModel,
202    // Change info omitted for now - can add if needed
203}
204
205/// Model information from store config response
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct StoreConfigModel {
208    /// Model ID (ULID)
209    pub id: String,
210    /// Model type ("gateway", "pipeline", "transform")
211    #[serde(rename = "type")]
212    pub model_type: String,
213    /// Action taken ("created", "updated")
214    pub action: String,
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_store_config_request_with_id() {
223        let request = StoreConfigRequest {
224            config_type: "gateway".to_string(),
225            id: Some("01k8ek6h9aahhnrv3benret1nn".to_string()),
226            config: "[proxy]\nid = \"test\"\n".to_string(),
227        };
228
229        let json = serde_json::to_string(&request).unwrap();
230        assert!(json.contains("\"type\":\"gateway\""));
231        assert!(json.contains("\"id\":\"01k8ek6h9aahhnrv3benret1nn\""));
232        assert!(json.contains("\"config\":"));
233        assert!(json.contains("[proxy]"));
234
235        // Test deserialization
236        let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
237        assert_eq!(deserialized.config_type, "gateway");
238        assert_eq!(
239            deserialized.id,
240            Some("01k8ek6h9aahhnrv3benret1nn".to_string())
241        );
242    }
243
244    #[test]
245    fn test_store_config_request_without_id() {
246        let request = StoreConfigRequest {
247            config_type: "pipeline".to_string(),
248            id: None,
249            config: "[pipeline]\nname = \"test\"\n".to_string(),
250        };
251
252        let json = serde_json::to_string(&request).unwrap();
253        assert!(json.contains("\"type\":\"pipeline\""));
254        assert!(json.contains("\"config\":"));
255        // Should not contain the id field when None
256        assert!(!json.contains("\"id\""));
257
258        // Test deserialization
259        let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
260        assert_eq!(deserialized.config_type, "pipeline");
261        assert_eq!(deserialized.id, None);
262    }
263
264    #[test]
265    fn test_store_config_request_type_field_rename() {
266        // Test that the "type" field is correctly serialized despite the field being named config_type
267        let json = r#"{"type":"transform","config":"[transform]\nname = \"test\"\n"}"#;
268        let request: StoreConfigRequest = serde_json::from_str(json).unwrap();
269        assert_eq!(request.config_type, "transform");
270        assert_eq!(request.id, None);
271    }
272
273    #[test]
274    fn test_store_config_response() {
275        let json = r#"{
276            "success": true,
277            "message": "Gateway configuration updated successfully",
278            "data": {
279                "model": {
280                    "id": "01k9npa4tatmwddk66xxpcr2r0",
281                    "type": "gateway",
282                    "action": "updated"
283                },
284                "change": {}
285            }
286        }"#;
287
288        let response: StoreConfigResponse = serde_json::from_str(json).unwrap();
289        assert_eq!(response.success, true);
290        assert!(response.message.contains("updated successfully"));
291        assert_eq!(response.data.model.id, "01k9npa4tatmwddk66xxpcr2r0");
292        assert_eq!(response.data.model.model_type, "gateway");
293        assert_eq!(response.data.model.action, "updated");
294    }
295}