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