1mod error;
47mod profile;
48mod ws;
49
50pub use cloacina_api_types as types;
51pub use error::ClientError;
52pub use profile::resolve_api_key_scheme;
53pub use ws::{DeliveryPush, SubscribeOptions, DELIVERY_PROTOCOL_VERSION};
54
55use std::sync::Arc;
56use std::time::Duration;
57
58use reqwest::{Method, Response};
59use serde::de::DeserializeOwned;
60use serde_json::Value;
61
62use cloacina_api_types::{
63 AccumulatorStatus, AgentInfo, CompilerStatus, CreateKeyRequest, CreateTenantRequest,
64 DeclaredSurface, ExecuteRequest, ExecuteResponse, ExecutionDetail, ExecutionEventsResponse,
65 ExecutionSummary, ExecutionTasksResponse, FireReactorRequest, FireReactorResponse,
66 FireTriggerRequest, FireTriggerResponse, GraphStatus, InjectAccumulatorRequest,
67 InjectAccumulatorResponse, KeyCreatedResponse, KeyInfo, KeyRevokedResponse, KeyRole,
68 ListResponse, ReactorFire, ReactorFireTimeseries, ReactorStatus, TenantCreatedResponse,
69 TenantListResponse, TenantRemovedResponse, TenantSummary, TriggerDetailResponse,
70 TriggerPauseResponse, TriggerScheduleSummary, WorkflowDeletedResponse, WorkflowDetail,
71 WorkflowPauseResponse, WorkflowSourceResponse, WorkflowSummary, WorkflowUploadedResponse,
72 WsTicketResponse,
73};
74
75#[derive(Debug, Clone, Default)]
77pub struct ClientBuilder {
78 server: String,
79 api_key: Option<String>,
80 tenant: Option<String>,
81 connect_timeout: Option<Duration>,
82 timeout: Option<Duration>,
83}
84
85impl ClientBuilder {
86 pub fn new(server: impl Into<String>) -> Self {
89 Self {
90 server: server.into(),
91 ..Default::default()
92 }
93 }
94
95 pub fn from_cloacinactl_profile(
99 home: Option<&std::path::Path>,
100 profile: Option<&str>,
101 ) -> Result<Self, ClientError> {
102 profile::builder_from_profile(home, profile)
103 }
104
105 pub fn api_key(mut self, key: impl Into<String>) -> Self {
107 self.api_key = Some(key.into());
108 self
109 }
110
111 pub fn tenant(mut self, tenant: impl Into<String>) -> Self {
114 self.tenant = Some(tenant.into());
115 self
116 }
117
118 pub fn connect_timeout(mut self, d: Duration) -> Self {
120 self.connect_timeout = Some(d);
121 self
122 }
123
124 pub fn timeout(mut self, d: Duration) -> Self {
126 self.timeout = Some(d);
127 self
128 }
129
130 pub fn build(self) -> Result<Client, ClientError> {
131 let api_key = self
132 .api_key
133 .ok_or_else(|| ClientError::Config("no API key configured".into()))?;
134 let http = reqwest::Client::builder()
135 .connect_timeout(self.connect_timeout.unwrap_or(Duration::from_secs(5)))
136 .timeout(self.timeout.unwrap_or(Duration::from_secs(30)))
137 .build()
138 .map_err(ClientError::from_reqwest)?;
139 Ok(Client {
140 inner: Arc::new(ClientInner {
141 server: self.server.trim_end_matches('/').to_string(),
142 api_key,
143 tenant: self.tenant,
144 http,
145 }),
146 })
147 }
148}
149
150struct ClientInner {
151 server: String,
152 api_key: String,
153 tenant: Option<String>,
154 http: reqwest::Client,
155}
156
157#[derive(Clone)]
160pub struct Client {
161 inner: Arc<ClientInner>,
162}
163
164impl Client {
165 pub fn server(&self) -> &str {
167 &self.inner.server
168 }
169
170 pub fn tenant_segment(&self) -> &str {
173 self.inner.tenant.as_deref().unwrap_or("public")
174 }
175
176 fn url(&self, path: &str) -> String {
177 format!("{}/{}", self.inner.server, path.trim_start_matches('/'))
178 }
179
180 fn request(&self, method: Method, path: &str) -> reqwest::RequestBuilder {
181 self.inner
184 .http
185 .request(method, self.url(path))
186 .bearer_auth(&self.inner.api_key)
187 }
188
189 async fn parse<T: DeserializeOwned>(response: Response) -> Result<T, ClientError> {
190 let status = response.status().as_u16();
191 if response.status().is_success() {
192 return response
193 .json::<T>()
194 .await
195 .map_err(ClientError::from_reqwest);
196 }
197 let body = response.json::<Value>().await.unwrap_or(Value::Null);
198 Err(ClientError::from_status(status, body))
199 }
200
201 pub async fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T, ClientError> {
207 let response = self
208 .request(Method::GET, path)
209 .send()
210 .await
211 .map_err(ClientError::from_reqwest)?;
212 Self::parse(response).await
213 }
214
215 pub async fn post_json<B: serde::Serialize + ?Sized, T: DeserializeOwned>(
217 &self,
218 path: &str,
219 body: &B,
220 ) -> Result<T, ClientError> {
221 let response = self
222 .request(Method::POST, path)
223 .json(body)
224 .send()
225 .await
226 .map_err(ClientError::from_reqwest)?;
227 Self::parse(response).await
228 }
229
230 pub async fn delete_path(&self, path: &str) -> Result<(), ClientError> {
232 let response = self
233 .request(Method::DELETE, path)
234 .send()
235 .await
236 .map_err(ClientError::from_reqwest)?;
237 let status = response.status().as_u16();
238 if response.status().is_success() {
239 return Ok(());
240 }
241 let body = response.json::<Value>().await.unwrap_or(Value::Null);
242 Err(ClientError::from_status(status, body))
243 }
244
245 fn tenant_of<'a>(&'a self, tenant: Option<&'a str>) -> &'a str {
246 tenant.unwrap_or_else(|| self.tenant_segment())
247 }
248
249 pub async fn health(&self) -> Result<Value, ClientError> {
252 self.get_json("/health").await
253 }
254
255 pub async fn ready(&self) -> Result<(u16, Value), ClientError> {
257 let response = self
258 .request(Method::GET, "/ready")
259 .send()
260 .await
261 .map_err(ClientError::from_reqwest)?;
262 let status = response.status().as_u16();
263 let body = response.json::<Value>().await.unwrap_or(Value::Null);
264 Ok((status, body))
265 }
266
267 pub async fn create_key(
270 &self,
271 name: &str,
272 role: KeyRole,
273 ) -> Result<KeyCreatedResponse, ClientError> {
274 self.post_json(
275 "/v1/auth/keys",
276 &CreateKeyRequest {
277 name: name.to_string(),
278 role,
279 },
280 )
281 .await
282 }
283
284 pub async fn list_keys(&self) -> Result<ListResponse<KeyInfo>, ClientError> {
285 self.get_json("/v1/auth/keys").await
286 }
287
288 pub async fn revoke_key(&self, key_id: &str) -> Result<KeyRevokedResponse, ClientError> {
289 let response = self
290 .request(Method::DELETE, &format!("/v1/auth/keys/{key_id}"))
291 .send()
292 .await
293 .map_err(ClientError::from_reqwest)?;
294 Self::parse(response).await
295 }
296
297 pub async fn create_tenant_key(
298 &self,
299 name: &str,
300 role: KeyRole,
301 tenant: Option<&str>,
302 ) -> Result<KeyCreatedResponse, ClientError> {
303 let t = self.tenant_of(tenant);
304 self.post_json(
305 &format!("/v1/tenants/{t}/keys"),
306 &CreateKeyRequest {
307 name: name.to_string(),
308 role,
309 },
310 )
311 .await
312 }
313
314 pub async fn list_tenant_keys(
317 &self,
318 tenant: Option<&str>,
319 ) -> Result<ListResponse<KeyInfo>, ClientError> {
320 let t = self.tenant_of(tenant);
321 self.get_json(&format!("/v1/tenants/{t}/keys")).await
322 }
323
324 pub async fn revoke_tenant_key(
328 &self,
329 key_id: &str,
330 tenant: Option<&str>,
331 ) -> Result<KeyRevokedResponse, ClientError> {
332 let t = self.tenant_of(tenant);
333 let response = self
334 .request(Method::DELETE, &format!("/v1/tenants/{t}/keys/{key_id}"))
335 .send()
336 .await
337 .map_err(ClientError::from_reqwest)?;
338 Self::parse(response).await
339 }
340
341 pub async fn create_ws_ticket(&self) -> Result<WsTicketResponse, ClientError> {
343 self.post_json("/v1/auth/ws-ticket", &Value::Null).await
344 }
345
346 pub async fn local_login(
356 &self,
357 username: &str,
358 password: &str,
359 tenant: Option<&str>,
360 ) -> Result<Value, ClientError> {
361 let body = serde_json::json!({
362 "username": username,
363 "password": password,
364 "tenant": tenant,
365 });
366 self.post_json("/v1/auth/local/login", &body).await
367 }
368
369 pub async fn refresh(&self) -> Result<Value, ClientError> {
371 self.post_json("/v1/auth/refresh", &Value::Null).await
372 }
373
374 pub async fn logout(&self) -> Result<Value, ClientError> {
376 self.post_json("/v1/auth/logout", &Value::Null).await
377 }
378
379 pub async fn whoami(&self) -> Result<Value, ClientError> {
381 self.get_json("/v1/auth/whoami").await
382 }
383
384 pub async fn list_accounts(&self, tenant: Option<&str>) -> Result<Value, ClientError> {
388 let t = self.tenant_of(tenant);
389 self.get_json(&format!("/v1/tenants/{t}/accounts")).await
390 }
391
392 pub async fn create_account(
394 &self,
395 username: &str,
396 password: &str,
397 role: &str,
398 tenant: Option<&str>,
399 ) -> Result<Value, ClientError> {
400 let t = self.tenant_of(tenant);
401 let body = serde_json::json!({
402 "username": username,
403 "password": password,
404 "role": role,
405 });
406 self.post_json(&format!("/v1/tenants/{t}/accounts"), &body)
407 .await
408 }
409
410 pub async fn disable_account(
412 &self,
413 account_id: &str,
414 tenant: Option<&str>,
415 ) -> Result<Value, ClientError> {
416 let t = self.tenant_of(tenant);
417 let response = self
418 .request(
419 Method::DELETE,
420 &format!("/v1/tenants/{t}/accounts/{account_id}"),
421 )
422 .send()
423 .await
424 .map_err(ClientError::from_reqwest)?;
425 Self::parse(response).await
426 }
427
428 pub async fn reset_password(
430 &self,
431 account_id: &str,
432 password: &str,
433 tenant: Option<&str>,
434 ) -> Result<Value, ClientError> {
435 let t = self.tenant_of(tenant);
436 let body = serde_json::json!({ "password": password });
437 self.post_json(
438 &format!("/v1/tenants/{t}/accounts/{account_id}/password"),
439 &body,
440 )
441 .await
442 }
443
444 pub async fn create_tenant(
447 &self,
448 request: &CreateTenantRequest,
449 ) -> Result<TenantCreatedResponse, ClientError> {
450 self.post_json("/v1/tenants", request).await
451 }
452
453 pub async fn list_tenants(&self) -> Result<ListResponse<TenantSummary>, ClientError> {
454 self.get_json("/v1/tenants").await
455 }
456
457 pub async fn remove_tenant(
458 &self,
459 schema_name: &str,
460 ) -> Result<TenantRemovedResponse, ClientError> {
461 let response = self
462 .request(Method::DELETE, &format!("/v1/tenants/{schema_name}"))
463 .send()
464 .await
465 .map_err(ClientError::from_reqwest)?;
466 Self::parse(response).await
467 }
468
469 pub async fn upload_workflow(
473 &self,
474 package: Vec<u8>,
475 tenant: Option<&str>,
476 ) -> Result<WorkflowUploadedResponse, ClientError> {
477 let t = self.tenant_of(tenant);
478 let part = reqwest::multipart::Part::bytes(package)
479 .file_name("package.cloacina")
480 .mime_str("application/octet-stream")
481 .map_err(ClientError::from_reqwest)?;
482 let form = reqwest::multipart::Form::new().part("file", part);
483 let response = self
484 .request(Method::POST, &format!("/v1/tenants/{t}/workflows"))
485 .multipart(form)
486 .send()
487 .await
488 .map_err(ClientError::from_reqwest)?;
489 Self::parse(response).await
490 }
491
492 pub async fn list_workflows(
493 &self,
494 tenant: Option<&str>,
495 ) -> Result<TenantListResponse<WorkflowSummary>, ClientError> {
496 let t = self.tenant_of(tenant);
497 self.get_json(&format!("/v1/tenants/{t}/workflows")).await
498 }
499
500 pub async fn get_workflow(
501 &self,
502 name: &str,
503 tenant: Option<&str>,
504 ) -> Result<WorkflowDetail, ClientError> {
505 let t = self.tenant_of(tenant);
506 self.get_json(&format!("/v1/tenants/{t}/workflows/{name}"))
507 .await
508 }
509
510 pub async fn delete_workflow(
511 &self,
512 name: &str,
513 version: &str,
514 tenant: Option<&str>,
515 ) -> Result<WorkflowDeletedResponse, ClientError> {
516 let t = self.tenant_of(tenant);
517 let response = self
518 .request(
519 Method::DELETE,
520 &format!("/v1/tenants/{t}/workflows/{name}/{version}"),
521 )
522 .send()
523 .await
524 .map_err(ClientError::from_reqwest)?;
525 Self::parse(response).await
526 }
527
528 pub async fn list_triggers(
531 &self,
532 limit: Option<i64>,
533 offset: Option<i64>,
534 tenant: Option<&str>,
535 ) -> Result<TenantListResponse<TriggerScheduleSummary>, ClientError> {
536 let t = self.tenant_of(tenant);
537 let mut path = format!("/v1/tenants/{t}/triggers");
538 let mut sep = '?';
539 if let Some(l) = limit {
540 path.push_str(&format!("{sep}limit={l}"));
541 sep = '&';
542 }
543 if let Some(o) = offset {
544 path.push_str(&format!("{sep}offset={o}"));
545 }
546 self.get_json(&path).await
547 }
548
549 pub async fn get_trigger(
550 &self,
551 name: &str,
552 tenant: Option<&str>,
553 ) -> Result<TriggerDetailResponse, ClientError> {
554 let t = self.tenant_of(tenant);
555 self.get_json(&format!("/v1/tenants/{t}/triggers/{name}"))
556 .await
557 }
558
559 pub async fn execute_workflow(
562 &self,
563 name: &str,
564 context: Value,
565 ) -> Result<ExecuteResponse, ClientError> {
566 let t = self.tenant_segment();
567 self.post_json(
568 &format!("/v1/tenants/{t}/workflows/{name}/execute"),
569 &ExecuteRequest {
570 context: Some(context),
571 },
572 )
573 .await
574 }
575
576 pub async fn list_executions(
577 &self,
578 query: &cloacina_api_types::ListExecutionsQuery,
579 tenant: Option<&str>,
580 ) -> Result<TenantListResponse<ExecutionSummary>, ClientError> {
581 let t = self.tenant_of(tenant);
582 let mut path = format!("/v1/tenants/{t}/executions");
583 let mut sep = '?';
584 let mut push = |k: &str, v: String| {
585 path.push_str(&format!("{sep}{k}={v}"));
586 sep = '&';
587 };
588 if let Some(s) = &query.status {
589 push("status", urlencoding::encode(s).into_owned());
590 }
591 if let Some(w) = &query.workflow {
592 push("workflow", urlencoding::encode(w).into_owned());
593 }
594 if let Some(l) = query.limit {
595 push("limit", l.to_string());
596 }
597 if let Some(o) = query.offset {
598 push("offset", o.to_string());
599 }
600 self.get_json(&path).await
601 }
602
603 pub async fn get_execution(
604 &self,
605 exec_id: &str,
606 tenant: Option<&str>,
607 ) -> Result<ExecutionDetail, ClientError> {
608 let t = self.tenant_of(tenant);
609 self.get_json(&format!("/v1/tenants/{t}/executions/{exec_id}"))
610 .await
611 }
612
613 pub async fn get_execution_events(
614 &self,
615 exec_id: &str,
616 tenant: Option<&str>,
617 ) -> Result<ExecutionEventsResponse, ClientError> {
618 let t = self.tenant_of(tenant);
619 self.get_json(&format!("/v1/tenants/{t}/executions/{exec_id}/events"))
620 .await
621 }
622
623 pub async fn get_execution_tasks(
624 &self,
625 tenant_id: &str,
626 exec_id: &str,
627 ) -> Result<ExecutionTasksResponse, ClientError> {
628 self.get_json(&format!(
629 "/v1/tenants/{tenant_id}/executions/{exec_id}/tasks"
630 ))
631 .await
632 }
633
634 pub async fn list_accumulators(&self) -> Result<ListResponse<AccumulatorStatus>, ClientError> {
637 self.get_json("/v1/health/accumulators").await
638 }
639
640 pub async fn list_graphs(&self) -> Result<ListResponse<GraphStatus>, ClientError> {
641 self.get_json("/v1/health/graphs").await
642 }
643
644 pub async fn get_graph(&self, name: &str) -> Result<GraphStatus, ClientError> {
645 self.get_json(&format!("/v1/health/graphs/{name}")).await
646 }
647
648 pub async fn list_reactors(&self) -> Result<ListResponse<ReactorStatus>, ClientError> {
649 self.get_json("/v1/health/reactors").await
650 }
651
652 pub async fn fire_reactor(
655 &self,
656 name: &str,
657 request: &FireReactorRequest,
658 ) -> Result<FireReactorResponse, ClientError> {
659 self.post_json(&format!("/v1/health/reactors/{name}/fire"), request)
660 .await
661 }
662
663 pub async fn list_reactor_fires(
664 &self,
665 name: &str,
666 ) -> Result<ListResponse<ReactorFire>, ClientError> {
667 self.get_json(&format!("/v1/health/reactors/{name}/fires"))
668 .await
669 }
670
671 pub async fn reactor_fire_timeseries(
672 &self,
673 name: &str,
674 ) -> Result<ReactorFireTimeseries, ClientError> {
675 self.get_json(&format!("/v1/health/reactors/{name}/fires/timeseries"))
676 .await
677 }
678
679 pub async fn reactor_interface(&self, name: &str) -> Result<DeclaredSurface, ClientError> {
680 self.get_json(&format!("/v1/health/reactors/{name}/interface"))
681 .await
682 }
683
684 pub async fn accumulator_interface(&self, name: &str) -> Result<DeclaredSurface, ClientError> {
685 self.get_json(&format!("/v1/health/accumulators/{name}/interface"))
686 .await
687 }
688
689 pub async fn inject_accumulator(
690 &self,
691 name: &str,
692 request: &InjectAccumulatorRequest,
693 ) -> Result<InjectAccumulatorResponse, ClientError> {
694 self.post_json(&format!("/v1/health/accumulators/{name}/inject"), request)
695 .await
696 }
697
698 pub async fn pause_workflow(
701 &self,
702 name: &str,
703 tenant: Option<&str>,
704 ) -> Result<WorkflowPauseResponse, ClientError> {
705 let t = self.tenant_of(tenant);
706 self.post_json(
707 &format!("/v1/tenants/{t}/workflows/{name}/pause"),
708 &Value::Null,
709 )
710 .await
711 }
712
713 pub async fn resume_workflow(
714 &self,
715 name: &str,
716 tenant: Option<&str>,
717 ) -> Result<WorkflowPauseResponse, ClientError> {
718 let t = self.tenant_of(tenant);
719 self.post_json(
720 &format!("/v1/tenants/{t}/workflows/{name}/resume"),
721 &Value::Null,
722 )
723 .await
724 }
725
726 pub async fn get_workflow_source(
727 &self,
728 name: &str,
729 tenant: Option<&str>,
730 ) -> Result<WorkflowSourceResponse, ClientError> {
731 let t = self.tenant_of(tenant);
732 self.get_json(&format!("/v1/tenants/{t}/workflows/{name}/source"))
733 .await
734 }
735
736 pub async fn pause_trigger(
737 &self,
738 name: &str,
739 tenant: Option<&str>,
740 ) -> Result<TriggerPauseResponse, ClientError> {
741 let t = self.tenant_of(tenant);
742 self.post_json(
743 &format!("/v1/tenants/{t}/triggers/{name}/pause"),
744 &Value::Null,
745 )
746 .await
747 }
748
749 pub async fn fire_trigger(
752 &self,
753 name: &str,
754 request: &FireTriggerRequest,
755 tenant: Option<&str>,
756 ) -> Result<FireTriggerResponse, ClientError> {
757 let t = self.tenant_of(tenant);
758 self.post_json(&format!("/v1/tenants/{t}/triggers/{name}/fire"), request)
759 .await
760 }
761
762 pub async fn trigger_interface(
765 &self,
766 name: &str,
767 tenant: Option<&str>,
768 ) -> Result<DeclaredSurface, ClientError> {
769 let t = self.tenant_of(tenant);
770 self.get_json(&format!("/v1/tenants/{t}/triggers/{name}/interface"))
771 .await
772 }
773
774 pub async fn resume_trigger(
775 &self,
776 name: &str,
777 tenant: Option<&str>,
778 ) -> Result<TriggerPauseResponse, ClientError> {
779 let t = self.tenant_of(tenant);
780 self.post_json(
781 &format!("/v1/tenants/{t}/triggers/{name}/resume"),
782 &Value::Null,
783 )
784 .await
785 }
786
787 pub async fn list_agents(&self) -> Result<ListResponse<AgentInfo>, ClientError> {
790 self.get_json("/v1/agents").await
791 }
792
793 pub async fn compiler_status(&self) -> Result<CompilerStatus, ClientError> {
794 self.get_json("/v1/compiler/status").await
795 }
796
797 pub fn subscribe_delivery(
803 &self,
804 recipient: &str,
805 options: SubscribeOptions,
806 ) -> impl futures_util::Stream<Item = Result<DeliveryPush, ClientError>> + '_ {
807 ws::subscribe_delivery(self.clone(), recipient.to_string(), options)
808 }
809
810 pub fn follow_execution_events(
814 &self,
815 execution_id: &str,
816 ) -> impl futures_util::Stream<Item = Result<Value, ClientError>> + '_ {
817 self.follow_execution_events_with(execution_id, SubscribeOptions::default())
818 }
819
820 pub fn follow_execution_events_with(
823 &self,
824 execution_id: &str,
825 options: SubscribeOptions,
826 ) -> impl futures_util::Stream<Item = Result<Value, ClientError>> + '_ {
827 ws::follow_execution_events(self.clone(), execution_id.to_string(), options)
828 }
829}