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: StoreConfigModel,
204}
205
206/// Model information from store config response
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct StoreConfigModel {
209    /// Model ID (ULID)
210    pub id: String,
211    /// Model type ("gateway", "pipeline", "transform")
212    #[serde(rename = "type")]
213    pub model_type: String,
214    /// Action taken ("created", "updated")
215    pub action: String,
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_store_config_request_with_id() {
224        let request = StoreConfigRequest {
225            config_type: "gateway".to_string(),
226            id: Some("01k8ek6h9aahhnrv3benret1nn".to_string()),
227            config: "[proxy]\nid = \"test\"\n".to_string(),
228        };
229
230        let json = serde_json::to_string(&request).unwrap();
231        assert!(json.contains("\"type\":\"gateway\""));
232        assert!(json.contains("\"id\":\"01k8ek6h9aahhnrv3benret1nn\""));
233        assert!(json.contains("\"config\":"));
234        assert!(json.contains("[proxy]"));
235
236        // Test deserialization
237        let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
238        assert_eq!(deserialized.config_type, "gateway");
239        assert_eq!(
240            deserialized.id,
241            Some("01k8ek6h9aahhnrv3benret1nn".to_string())
242        );
243    }
244
245    #[test]
246    fn test_store_config_request_without_id() {
247        let request = StoreConfigRequest {
248            config_type: "pipeline".to_string(),
249            id: None,
250            config: "[pipeline]\nname = \"test\"\n".to_string(),
251        };
252
253        let json = serde_json::to_string(&request).unwrap();
254        assert!(json.contains("\"type\":\"pipeline\""));
255        assert!(json.contains("\"config\":"));
256        // Should not contain the id field when None
257        assert!(!json.contains("\"id\""));
258
259        // Test deserialization
260        let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
261        assert_eq!(deserialized.config_type, "pipeline");
262        assert_eq!(deserialized.id, None);
263    }
264
265    #[test]
266    fn test_store_config_request_type_field_rename() {
267        // Test that the "type" field is correctly serialized despite the field being named config_type
268        let json = r#"{"type":"transform","config":"[transform]\nname = \"test\"\n"}"#;
269        let request: StoreConfigRequest = serde_json::from_str(json).unwrap();
270        assert_eq!(request.config_type, "transform");
271        assert_eq!(request.id, None);
272    }
273
274    #[test]
275    fn test_store_config_response() {
276        let json = r#"{
277            "success": true,
278            "message": "Gateway configuration updated successfully",
279            "data": {
280                "id": "01k9npa4tatmwddk66xxpcr2r0",
281                "type": "gateway",
282                "action": "updated"
283            }
284        }"#;
285
286        let response: StoreConfigResponse = serde_json::from_str(json).unwrap();
287        assert_eq!(response.success, true);
288        assert!(response.message.contains("updated successfully"));
289        assert_eq!(response.data.id, "01k9npa4tatmwddk66xxpcr2r0");
290        assert_eq!(response.data.model_type, "gateway");
291        assert_eq!(response.data.action, "updated");
292    }
293}