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#[derive(Debug, Clone)]
25pub struct RainEngineClient {
26 base_url: Url,
27 http: Client,
28}
29
30impl RainEngineClient {
31 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn health(&self) -> Result<Value, ClientError> {
231 let url = self.base_url.join("health")?;
232 self.get(url).await
233 }
234
235 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 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 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 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}