Skip to main content

rain_engine_client/
lib.rs

1use rain_engine_core::ApprovalDecision;
2use rain_engine_runtime::{
3    ApprovalIngressRequest, DelegationResultIngressRequest, EventIngressRequest,
4    HumanInputIngressRequest, RuntimeRunResult, ScheduledWakeIngressRequest, WebhookIngressRequest,
5};
6use reqwest::{Client, Url};
7use serde_json::Value;
8use thiserror::Error;
9
10#[derive(Debug, Error)]
11pub enum ClientError {
12    #[error("HTTP error: {0}")]
13    Http(#[from] reqwest::Error),
14    #[error("API error: {status} - {message}")]
15    Api { status: u16, message: String },
16    #[error("URL parsing error: {0}")]
17    Url(#[from] url::ParseError),
18}
19
20/// A client for the RainEngine Gateway.
21///
22/// Provides strongly-typed async methods for every ingress route exposed by
23/// the runtime HTTP server.
24#[derive(Debug, Clone)]
25pub struct RainEngineClient {
26    base_url: Url,
27    http: Client,
28}
29
30impl RainEngineClient {
31    /// Creates a new client connecting to the given base URL.
32    pub fn new(base_url: &str) -> Result<Self, ClientError> {
33        let mut base_url = Url::parse(base_url)?;
34        if !base_url.path().ends_with('/') {
35            base_url.set_path(&format!("{}/", base_url.path()));
36        }
37
38        Ok(Self {
39            base_url,
40            http: Client::new(),
41        })
42    }
43
44    /// Creates a new client with custom request headers (e.g. for future auth).
45    pub fn with_headers(
46        base_url: &str,
47        headers: reqwest::header::HeaderMap,
48    ) -> Result<Self, ClientError> {
49        let mut base_url = Url::parse(base_url)?;
50        if !base_url.path().ends_with('/') {
51            base_url.set_path(&format!("{}/", base_url.path()));
52        }
53
54        let http = Client::builder()
55            .default_headers(headers)
56            .build()
57            .map_err(ClientError::Http)?;
58
59        Ok(Self { base_url, http })
60    }
61
62    // ── Trigger: Human Input ────────────────────────────────────────────
63
64    /// Sends human input to a specific actor.
65    /// Route: `POST /triggers/human/{actor_id}`
66    pub async fn send_human_input(
67        &self,
68        actor_id: &str,
69        session_id: &str,
70        content: &str,
71    ) -> Result<RuntimeRunResult, ClientError> {
72        let url = self
73            .base_url
74            .join(&format!("triggers/human/{}", actor_id))?;
75        let request = HumanInputIngressRequest {
76            session_id: session_id.to_string(),
77            content: content.to_string(),
78            attachments: vec![],
79            granted_scopes: Default::default(),
80            idempotency_key: None,
81            provider: None,
82            policy_override: None,
83        };
84
85        self.post(url, &request).await
86    }
87
88    /// Sends a fully-specified human input request.
89    /// Route: `POST /triggers/human/{actor_id}`
90    pub async fn send_human_input_request(
91        &self,
92        actor_id: &str,
93        request: &HumanInputIngressRequest,
94    ) -> Result<RuntimeRunResult, ClientError> {
95        let url = self
96            .base_url
97            .join(&format!("triggers/human/{}", actor_id))?;
98        self.post(url, request).await
99    }
100
101    // ── Trigger: Approval ───────────────────────────────────────────────
102
103    /// Submits an approval decision.
104    /// Route: `POST /triggers/approval`
105    pub async fn submit_approval(
106        &self,
107        session_id: &str,
108        resume_token: &str,
109        decision: ApprovalDecision,
110        metadata: Value,
111    ) -> Result<RuntimeRunResult, ClientError> {
112        let url = self.base_url.join("triggers/approval")?;
113        let request = ApprovalIngressRequest {
114            session_id: session_id.to_string(),
115            resume_token: resume_token.to_string(),
116            decision,
117            metadata,
118            granted_scopes: Default::default(),
119            provider: None,
120            policy_override: None,
121        };
122
123        self.post(url, &request).await
124    }
125
126    /// Submits a fully-specified approval request.
127    /// Route: `POST /triggers/approval`
128    pub async fn submit_approval_request(
129        &self,
130        request: &ApprovalIngressRequest,
131    ) -> Result<RuntimeRunResult, ClientError> {
132        let url = self.base_url.join("triggers/approval")?;
133        self.post(url, request).await
134    }
135
136    // ── Trigger: Webhook ────────────────────────────────────────────────
137
138    /// Sends a webhook event from the given source.
139    /// Route: `POST /triggers/webhook/{source}`
140    pub async fn send_webhook(
141        &self,
142        source: &str,
143        request: &WebhookIngressRequest,
144    ) -> Result<RuntimeRunResult, ClientError> {
145        let url = self
146            .base_url
147            .join(&format!("triggers/webhook/{}", source))?;
148        self.post(url, request).await
149    }
150
151    // ── Trigger: External Event ─────────────────────────────────────────
152
153    /// Sends an external event from the given source.
154    /// Route: `POST /triggers/external/{source}`
155    pub async fn send_external_event(
156        &self,
157        source: &str,
158        request: &EventIngressRequest,
159    ) -> Result<RuntimeRunResult, ClientError> {
160        let url = self
161            .base_url
162            .join(&format!("triggers/external/{}", source))?;
163        self.post(url, request).await
164    }
165
166    // ── Trigger: System Observation ─────────────────────────────────────
167
168    /// Sends a system observation from the given source.
169    /// Route: `POST /triggers/system/{source}`
170    pub async fn send_system_observation(
171        &self,
172        source: &str,
173        request: &EventIngressRequest,
174    ) -> Result<RuntimeRunResult, ClientError> {
175        let url = self.base_url.join(&format!("triggers/system/{}", source))?;
176        self.post(url, request).await
177    }
178
179    // ── Trigger: Scheduled Wake ─────────────────────────────────────────
180
181    /// Sends a scheduled wake event with simple args.
182    /// Route: `POST /triggers/wake`
183    pub async fn send_scheduled_wake(
184        &self,
185        session_id: &str,
186        wake_id: &str,
187        due_at: i64,
188        reason: &str,
189    ) -> Result<RuntimeRunResult, ClientError> {
190        let url = self.base_url.join("triggers/wake")?;
191        let due_at = std::time::UNIX_EPOCH + std::time::Duration::from_millis(due_at as u64);
192        let request = ScheduledWakeIngressRequest {
193            session_id: session_id.to_string(),
194            wake_id: wake_id.to_string(),
195            due_at,
196            reason: reason.to_string(),
197            granted_scopes: Default::default(),
198            provider: None,
199            policy_override: None,
200        };
201        self.post(url, &request).await
202    }
203
204    /// Sends a fully-specified scheduled wake request.
205    /// Route: `POST /triggers/wake`
206    pub async fn send_scheduled_wake_request(
207        &self,
208        request: &ScheduledWakeIngressRequest,
209    ) -> Result<RuntimeRunResult, ClientError> {
210        let url = self.base_url.join("triggers/wake")?;
211        self.post(url, request).await
212    }
213
214    // ── Trigger: Delegation Result ──────────────────────────────────────
215
216    /// Sends a delegation result back to the engine.
217    /// Route: `POST /triggers/delegation-result`
218    pub async fn send_delegation_result(
219        &self,
220        request: &DelegationResultIngressRequest,
221    ) -> Result<RuntimeRunResult, ClientError> {
222        let url = self.base_url.join("triggers/delegation-result")?;
223        self.post(url, request).await
224    }
225
226    // ── Read: Health ────────────────────────────────────────────────────
227
228    /// Check the runtime health.
229    /// Route: `GET /health`
230    pub async fn health(&self) -> Result<Value, ClientError> {
231        let url = self.base_url.join("health")?;
232        self.get(url).await
233    }
234
235    // ── Read: Sessions ──────────────────────────────────────────────────
236
237    /// List all sessions.
238    /// Route: `GET /sessions`
239    pub async fn list_sessions(&self) -> Result<Value, ClientError> {
240        let url = self.base_url.join("sessions")?;
241        self.get(url).await
242    }
243
244    /// Get a session snapshot by ID.
245    /// Route: `GET /sessions/{session_id}`
246    pub async fn get_session(&self, session_id: &str) -> Result<Value, ClientError> {
247        let url = self.base_url.join(&format!("sessions/{}", session_id))?;
248        self.get(url).await
249    }
250
251    /// List records in a session with pagination.
252    /// Route: `GET /sessions/{session_id}/records`
253    pub async fn list_records(
254        &self,
255        session_id: &str,
256        offset: usize,
257        limit: usize,
258    ) -> Result<Value, ClientError> {
259        let url = self.base_url.join(&format!(
260            "sessions/{}/records?offset={}&limit={}",
261            session_id, offset, limit
262        ))?;
263        self.get(url).await
264    }
265
266    // ── Internal ────────────────────────────────────────────────────────
267
268    async fn post<T: serde::Serialize>(
269        &self,
270        url: Url,
271        payload: &T,
272    ) -> Result<RuntimeRunResult, ClientError> {
273        let response = self.http.post(url).json(payload).send().await?;
274        if response.status().is_success() {
275            let result = response.json::<RuntimeRunResult>().await?;
276            Ok(result)
277        } else {
278            Err(ClientError::Api {
279                status: response.status().as_u16(),
280                message: response.text().await.unwrap_or_default(),
281            })
282        }
283    }
284
285    async fn get(&self, url: Url) -> Result<Value, ClientError> {
286        let response = self.http.get(url).send().await?;
287        if response.status().is_success() {
288            let result = response.json::<Value>().await?;
289            Ok(result)
290        } else {
291            Err(ClientError::Api {
292                status: response.status().as_u16(),
293                message: response.text().await.unwrap_or_default(),
294            })
295        }
296    }
297}