use std::sync::Arc;
use std::time::Duration;
use anyhow::anyhow;
use reqwest::Method;
use super::JsonValue;
use crate::{
error::SdkError,
transport::{RequestSpec, Transport},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PollSchedule {
pub initial_delay: Duration,
pub max_delay: Duration,
pub timeout: Duration,
}
impl PollSchedule {
#[must_use]
pub const fn new(initial_delay: Duration, max_delay: Duration, timeout: Duration) -> Self {
Self {
initial_delay,
max_delay,
timeout,
}
}
}
impl Default for PollSchedule {
fn default() -> Self {
Self {
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(5),
timeout: Duration::from_mins(15),
}
}
}
#[derive(Debug, Clone)]
pub struct ScriptingClient {
transport: Arc<Transport>,
poll: PollSchedule,
}
impl ScriptingClient {
pub(crate) fn new(transport: Arc<Transport>) -> Self {
Self {
transport,
poll: PollSchedule::default(),
}
}
#[must_use]
pub fn with_poll_schedule(mut self, poll: PollSchedule) -> Self {
self.poll = poll;
self
}
pub async fn create_execution(&self, body: &JsonValue) -> Result<JsonValue, SdkError> {
self.transport
.request_json::<JsonValue, JsonValue>(RequestSpec {
method: Method::POST,
path: "/scripting/executions",
body: Some(body),
..Default::default()
})
.await
}
pub async fn get_execution(&self, id: &str) -> Result<JsonValue, SdkError> {
let path = format!("/scripting/executions/{id}");
self.transport
.request_json::<(), JsonValue>(RequestSpec {
method: Method::GET,
path: &path,
..Default::default()
})
.await
}
pub async fn list_executions(&self, limit: Option<i64>) -> Result<JsonValue, SdkError> {
let query = [("limit", limit.map(|n| n.to_string()))];
self.transport
.request_json::<(), JsonValue>(RequestSpec {
method: Method::GET,
path: "/scripting/executions",
query: &query,
..Default::default()
})
.await
}
pub async fn cancel_execution(&self, id: &str) -> Result<(), SdkError> {
let path = format!("/scripting/executions/{id}");
self.transport
.request_json::<(), ()>(RequestSpec {
method: Method::DELETE,
path: &path,
..Default::default()
})
.await
}
pub async fn execute(&self, body: &JsonValue) -> Result<JsonValue, SdkError> {
let created = self.create_execution(body).await?;
let id = created
.get("id")
.and_then(JsonValue::as_str)
.ok_or_else(|| {
SdkError::Deserialize(format!(
"POST /scripting/executions returned no string 'id': {created}"
))
})?
.to_owned();
let deadline = tokio::time::Instant::now() + self.poll.timeout;
let mut delay = self.poll.initial_delay;
loop {
tokio::time::sleep(delay).await;
let job = self.get_execution(&id).await?;
let status = job
.get("status")
.and_then(JsonValue::as_str)
.unwrap_or_default();
if is_terminal(status) {
return terminal_outcome(&job, status);
}
if tokio::time::Instant::now() >= deadline {
return Err(SdkError::Transport(format!(
"execution {id} did not finish within {:?}",
self.poll.timeout
)));
}
delay = (delay * 2).min(self.poll.max_delay);
}
}
}
fn is_terminal(status: &str) -> bool {
matches!(status, "succeeded" | "failed" | "canceled")
}
fn terminal_outcome(job: &JsonValue, status: &str) -> Result<JsonValue, SdkError> {
if status == "succeeded" {
return Ok(job.get("result").cloned().unwrap_or(JsonValue::Null));
}
let error = job
.get("error")
.and_then(JsonValue::as_str)
.unwrap_or("no error reported");
Err(SdkError::Other(anyhow!(
"script execution {status}: {error}"
)))
}