use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use futures_util::StreamExt;
use reqwest::header::{ACCEPT, CONTENT_TYPE};
use reqwest::multipart::{Form, Part};
use reqwest::{Method, Response};
use serde::Serialize;
use serde_json::{json, Value};
use crate::streaming::{
extract_run_result, iter_stream, wait_for_run_result, EventStream, StreamEventExt,
};
use crate::transport::{data_object, decode_data_response, enc, enc_path, Transport};
use crate::{Error, JsonObject, Result};
pub struct SubscribeWithEventOptions {
pub project: Option<String>,
pub reconnect: bool,
pub max_attempts: u32,
}
impl Default for SubscribeWithEventOptions {
fn default() -> Self {
SubscribeWithEventOptions {
project: None,
reconnect: true,
max_attempts: 5,
}
}
}
pub struct WaitOptions<'a> {
pub timeout: Duration,
pub on_chunk: Option<&'a mut (dyn FnMut(&str) + Send)>,
}
impl Default for WaitOptions<'_> {
fn default() -> Self {
WaitOptions {
timeout: Duration::from_secs(60),
on_chunk: None,
}
}
}
pub struct CallOptions<'a> {
pub timeout: Duration,
pub on_chunk: Option<&'a mut (dyn FnMut(&str) + Send)>,
}
impl Default for CallOptions<'_> {
fn default() -> Self {
CallOptions {
timeout: Duration::from_secs(60),
on_chunk: None,
}
}
}
pub struct BuildUpload {
pub content: Vec<u8>,
pub filename: Option<String>,
pub hash: String,
pub build_id: Option<String>,
}
macro_rules! resource {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
pub struct $name {
transport: Arc<Transport>,
}
impl $name {
pub(crate) fn new(transport: Arc<Transport>) -> Self {
Self { transport }
}
}
};
}
resource!(
EventsResource
);
resource!(
RunsResource
);
resource!(
EnvResource
);
resource!(
OrgResource
);
resource!(
StreamsResource
);
resource!(
FilesResource
);
resource!(
ProjectsResource
);
resource!(
BuildsResource
);
resource!(
ContextResource
);
resource!(
DomainsResource
);
resource!(
SessionsResource
);
resource!(
ServiceKeysResource
);
impl EventsResource {
pub async fn publish(&self, body: impl Serialize) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(
Method::POST,
"/events",
Some(&serde_json::to_value(body)?),
&[],
)
.await?;
Ok(data_object(envelope))
}
pub async fn call_hot(
&self,
fn_name: &str,
args: Vec<Value>,
opts: CallOptions<'_>,
) -> Result<Value> {
let body = json!({
"event_type": "hot:call",
"event_data": { "fn": fn_name, "args": args },
});
let Value::Object(body) = body else {
unreachable!()
};
let published = self.publish(body).await?;
let stream_id = string_field(&published, "stream_id");
let event_id = string_field(&published, "event_id");
let run = wait_for_run_result(
self.transport.clone(),
&stream_id,
&event_id,
opts.timeout,
opts.on_chunk,
)
.await?;
Ok(extract_run_result(run.get("result").cloned()))
}
pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
self.transport
.request_json(Method::GET, "/events", None, query)
.await
}
pub async fn get(&self, event_id: &str) -> Result<JsonObject> {
let path = format!("/events/{}", enc(event_id));
let envelope = self
.transport
.request_json(Method::GET, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn get_runs(&self, event_id: &str) -> Result<JsonObject> {
let path = format!("/events/{}/runs", enc(event_id));
self.transport
.request_json(Method::GET, &path, None, &[])
.await
}
}
impl RunsResource {
pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
self.transport
.request_json(Method::GET, "/runs", None, query)
.await
}
pub async fn stats(&self) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(Method::GET, "/runs/stats", None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn get(&self, run_id: &str) -> Result<JsonObject> {
let path = format!("/runs/{}", enc(run_id));
let envelope = self
.transport
.request_json(Method::GET, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
}
impl EnvResource {
pub async fn get(&self) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(Method::GET, "/env", None, &[])
.await?;
Ok(data_object(envelope))
}
pub fn subscribe(&self) -> EventStream {
iter_stream(
self.transport.clone(),
Method::GET,
"/env/subscribe".to_string(),
None,
Vec::new(),
)
}
}
impl OrgResource {
pub async fn usage(&self) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(Method::GET, "/org/usage", None, &[])
.await?;
Ok(data_object(envelope))
}
}
impl StreamsResource {
pub fn subscribe(&self, stream_id: &str, project: Option<&str>) -> EventStream {
iter_stream(
self.transport.clone(),
Method::GET,
format!("/streams/{}/subscribe", enc(stream_id)),
None,
project_query(project),
)
}
pub fn subscribe_post(&self, stream_id: &str, project: Option<&str>) -> EventStream {
iter_stream(
self.transport.clone(),
Method::POST,
format!("/streams/{}/subscribe", enc(stream_id)),
None,
project_query(project),
)
}
pub async fn wait_for_run_result(
&self,
stream_id: &str,
event_id: &str,
opts: WaitOptions<'_>,
) -> Result<JsonObject> {
wait_for_run_result(
self.transport.clone(),
stream_id,
event_id,
opts.timeout,
opts.on_chunk,
)
.await
}
pub fn subscribe_with_event(
&self,
body: impl Serialize,
opts: SubscribeWithEventOptions,
) -> EventStream {
let body = match serde_json::to_value(body) {
Ok(body) => body,
Err(error) => {
return Box::pin(async_stream::stream! { yield Err(Error::Json(error)) });
}
};
let transport = self.transport.clone();
let query = project_query(opts.project.as_deref());
let once = move |transport: Arc<Transport>, query: Vec<(String, String)>| {
iter_stream(
transport,
Method::POST,
"/streams/subscribe-with-event".to_string(),
Some(body.clone()),
query,
)
};
if !opts.reconnect {
return once(transport, query);
}
Box::pin(async_stream::stream! {
let mut seen_start: HashSet<String> = HashSet::new();
let mut seen_terminal: HashSet<String> = HashSet::new();
let mut stream_id: Option<String> = None;
let mut attempts: u32 = 0;
loop {
let mut source = match &stream_id {
None => once(transport.clone(), query.clone()),
Some(id) => iter_stream(
transport.clone(),
Method::GET,
format!("/streams/{}/subscribe", enc(id)),
None,
query.clone(),
),
};
let mut terminal = false;
while let Some(item) = source.next().await {
let event = match item {
Ok(event) => event,
Err(error) => {
if stream_id.is_none() || attempts >= opts.max_attempts {
yield Err(error);
return;
}
break;
}
};
match event.event_type() {
"event:published" => {
if let Some(published) =
event.get("stream_id").and_then(Value::as_str)
{
stream_id = Some(published.to_string());
}
}
"run:start" => {
let run_id = event.run_id().map(str::to_string);
if let Some(run_id) = run_id {
if !seen_start.insert(run_id) {
continue;
}
}
}
"run:stop" | "run:fail" | "run:cancel" => {
let run_id = event.run_id().map(str::to_string);
if let Some(run_id) = run_id {
if !seen_terminal.insert(run_id) {
continue;
}
}
terminal = true;
}
_ => {}
}
yield Ok(event);
}
if terminal {
return;
}
if stream_id.is_none() {
yield Err(Error::Protocol(
"stream ended before event:published was received".to_string(),
));
return;
}
attempts += 1;
let delay = Duration::from_millis((250 * u64::from(attempts)).min(2000));
tokio::time::sleep(delay).await;
}
})
}
}
impl FilesResource {
pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
self.transport
.request_json(Method::GET, "/files", None, query)
.await
}
pub async fn get(&self, file_id: &str) -> Result<JsonObject> {
let path = format!("/files/{}", enc(file_id));
let envelope = self
.transport
.request_json(Method::GET, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn delete(&self, file_id: &str) -> Result<()> {
let path = format!("/files/{}", enc(file_id));
let builder = self.transport.request_builder(Method::DELETE, &path);
self.transport.execute(builder).await?;
Ok(())
}
pub async fn download(&self, file_id: &str) -> Result<Response> {
let path = format!("/files/{}/download", enc(file_id));
let builder = self.transport.request_builder(Method::GET, &path);
self.transport.execute(builder).await
}
pub async fn upload(
&self,
path: &str,
content: Vec<u8>,
content_type: Option<&str>,
) -> Result<JsonObject> {
let request_path = format!("/files/upload/{}", enc_path(path));
let mut builder = self
.transport
.request_builder(Method::PUT, &request_path)
.header(ACCEPT, "application/json")
.body(content);
if let Some(content_type) = content_type {
builder = builder.header(CONTENT_TYPE, content_type);
}
let response = self.transport.execute(builder).await?;
decode_data_response(response).await
}
pub async fn initiate_upload(&self, body: impl Serialize) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(
Method::POST,
"/files/uploads",
Some(&serde_json::to_value(body)?),
&[],
)
.await?;
Ok(data_object(envelope))
}
pub async fn upload_part(
&self,
upload_id: &str,
part_number: u32,
content: Vec<u8>,
) -> Result<JsonObject> {
let path = format!("/files/uploads/{}/{}", enc(upload_id), part_number);
let builder = self
.transport
.request_builder(Method::PUT, &path)
.header(ACCEPT, "application/json")
.body(content);
let response = self.transport.execute(builder).await?;
decode_data_response(response).await
}
pub async fn complete_upload(&self, upload_id: &str) -> Result<JsonObject> {
let path = format!("/files/uploads/{}/complete", enc(upload_id));
let envelope = self
.transport
.request_json(Method::POST, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn abort_upload(&self, upload_id: &str) -> Result<()> {
let path = format!("/files/uploads/{}", enc(upload_id));
let builder = self.transport.request_builder(Method::DELETE, &path);
self.transport.execute(builder).await?;
Ok(())
}
}
impl ProjectsResource {
pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
self.transport
.request_json(Method::GET, "/projects", None, query)
.await
}
pub async fn create(&self, body: impl Serialize) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(
Method::POST,
"/projects",
Some(&serde_json::to_value(body)?),
&[],
)
.await?;
Ok(data_object(envelope))
}
pub async fn get(&self, project: &str) -> Result<JsonObject> {
let path = format!("/projects/{}", enc(project));
let envelope = self
.transport
.request_json(Method::GET, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn update(&self, project: &str, body: impl Serialize) -> Result<JsonObject> {
let path = format!("/projects/{}", enc(project));
let envelope = self
.transport
.request_json(
Method::PATCH,
&path,
Some(&serde_json::to_value(body)?),
&[],
)
.await?;
Ok(data_object(envelope))
}
pub async fn delete(&self, project: &str) -> Result<()> {
let path = format!("/projects/{}", enc(project));
let builder = self.transport.request_builder(Method::DELETE, &path);
self.transport.execute(builder).await?;
Ok(())
}
pub async fn activate(&self, project: &str) -> Result<JsonObject> {
let path = format!("/projects/{}/activate", enc(project));
let envelope = self
.transport
.request_json(Method::POST, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn deactivate(&self, project: &str) -> Result<JsonObject> {
let path = format!("/projects/{}/deactivate", enc(project));
let envelope = self
.transport
.request_json(Method::POST, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn event_handlers(&self, project: &str) -> Result<JsonObject> {
let path = format!("/projects/{}/event-handlers", enc(project));
self.transport
.request_json(Method::GET, &path, None, &[])
.await
}
pub async fn schedules(&self, project: &str) -> Result<JsonObject> {
let path = format!("/projects/{}/schedules", enc(project));
self.transport
.request_json(Method::GET, &path, None, &[])
.await
}
}
impl BuildsResource {
pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
self.transport
.request_json(Method::GET, "/builds", None, query)
.await
}
pub async fn list_for_project(
&self,
project: &str,
query: &[(&str, &str)],
) -> Result<JsonObject> {
let path = format!("/projects/{}/builds", enc(project));
self.transport
.request_json(Method::GET, &path, None, query)
.await
}
pub async fn get(&self, project: &str, build_id: &str) -> Result<JsonObject> {
let path = format!("/projects/{}/builds/{}", enc(project), enc(build_id));
let envelope = self
.transport
.request_json(Method::GET, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn deployed(&self, project: &str) -> Result<JsonObject> {
let path = format!("/projects/{}/builds/deployed", enc(project));
let envelope = self
.transport
.request_json(Method::GET, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn live(&self, project: &str) -> Result<JsonObject> {
let path = format!("/projects/{}/builds/live", enc(project));
let envelope = self
.transport
.request_json(Method::GET, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn upload(&self, project: &str, upload: BuildUpload) -> Result<JsonObject> {
let mut form = Form::new().text("hash", upload.hash);
if let Some(build_id) = upload.build_id {
form = form.text("build_id", build_id);
}
let part = Part::bytes(upload.content)
.file_name(upload.filename.unwrap_or_else(|| "build".to_string()));
form = form.part("file", part);
let path = format!("/projects/{}/builds", enc(project));
let builder = self
.transport
.request_builder(Method::POST, &path)
.header(ACCEPT, "application/json")
.multipart(form);
let response = self.transport.execute(builder).await?;
decode_data_response(response).await
}
pub async fn download(&self, project: &str, build_id: &str) -> Result<Response> {
let path = format!(
"/projects/{}/builds/{}/download",
enc(project),
enc(build_id)
);
let builder = self.transport.request_builder(Method::GET, &path);
self.transport.execute(builder).await
}
pub async fn deploy(&self, project: &str, build_id: &str) -> Result<JsonObject> {
let path = format!("/projects/{}/builds/{}/deploy", enc(project), enc(build_id));
let envelope = self
.transport
.request_json(Method::POST, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
}
impl ContextResource {
pub async fn list(&self, project: &str) -> Result<JsonObject> {
let path = format!("/projects/{}/context", enc(project));
self.transport
.request_json(Method::GET, &path, None, &[])
.await
}
pub async fn create(&self, project: &str, body: impl Serialize) -> Result<JsonObject> {
let path = format!("/projects/{}/context", enc(project));
let envelope = self
.transport
.request_json(Method::POST, &path, Some(&serde_json::to_value(body)?), &[])
.await?;
Ok(data_object(envelope))
}
pub async fn update(
&self,
project: &str,
key: &str,
body: impl Serialize,
) -> Result<JsonObject> {
let path = format!("/projects/{}/context/{}", enc(project), enc(key));
let envelope = self
.transport
.request_json(Method::PUT, &path, Some(&serde_json::to_value(body)?), &[])
.await?;
Ok(data_object(envelope))
}
pub async fn delete(&self, project: &str, key: &str) -> Result<()> {
let path = format!("/projects/{}/context/{}", enc(project), enc(key));
let builder = self.transport.request_builder(Method::DELETE, &path);
self.transport.execute(builder).await?;
Ok(())
}
}
impl DomainsResource {
pub async fn create(&self, body: impl Serialize) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(
Method::POST,
"/domains",
Some(&serde_json::to_value(body)?),
&[],
)
.await?;
Ok(data_object(envelope))
}
pub async fn list(&self) -> Result<JsonObject> {
self.transport
.request_json(Method::GET, "/domains", None, &[])
.await
}
pub async fn get(&self, domain_id: &str) -> Result<JsonObject> {
let path = format!("/domains/{}", enc(domain_id));
let envelope = self
.transport
.request_json(Method::GET, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn delete(&self, domain_id: &str) -> Result<()> {
let path = format!("/domains/{}", enc(domain_id));
let builder = self.transport.request_builder(Method::DELETE, &path);
self.transport.execute(builder).await?;
Ok(())
}
pub async fn verify(&self, domain_id: &str) -> Result<JsonObject> {
let path = format!("/domains/{}/verify", enc(domain_id));
let envelope = self
.transport
.request_json(Method::POST, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
}
impl SessionsResource {
pub async fn create(&self, body: impl Serialize) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(
Method::POST,
"/sessions",
Some(&serde_json::to_value(body)?),
&[],
)
.await?;
Ok(data_object(envelope))
}
pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
self.transport
.request_json(Method::GET, "/sessions", None, query)
.await
}
pub async fn revoke(&self, session_id: &str) -> Result<()> {
let path = format!("/sessions/{}", enc(session_id));
let builder = self.transport.request_builder(Method::DELETE, &path);
self.transport.execute(builder).await?;
Ok(())
}
pub async fn revoke_all(&self) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(Method::DELETE, "/sessions", None, &[])
.await?;
Ok(data_object(envelope))
}
}
impl ServiceKeysResource {
pub async fn create(&self, body: impl Serialize) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(
Method::POST,
"/service-keys",
Some(&serde_json::to_value(body)?),
&[],
)
.await?;
Ok(data_object(envelope))
}
pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
self.transport
.request_json(Method::GET, "/service-keys", None, query)
.await
}
pub async fn get(&self, service_key_id: &str) -> Result<JsonObject> {
let path = format!("/service-keys/{}", enc(service_key_id));
let envelope = self
.transport
.request_json(Method::GET, &path, None, &[])
.await?;
Ok(data_object(envelope))
}
pub async fn update(&self, service_key_id: &str, body: impl Serialize) -> Result<JsonObject> {
let path = format!("/service-keys/{}", enc(service_key_id));
let envelope = self
.transport
.request_json(
Method::PATCH,
&path,
Some(&serde_json::to_value(body)?),
&[],
)
.await?;
Ok(data_object(envelope))
}
pub async fn revoke(&self, service_key_id: &str) -> Result<()> {
let path = format!("/service-keys/{}", enc(service_key_id));
let builder = self.transport.request_builder(Method::DELETE, &path);
self.transport.execute(builder).await?;
Ok(())
}
pub async fn revoke_all(&self) -> Result<JsonObject> {
let envelope = self
.transport
.request_json(Method::DELETE, "/service-keys", None, &[])
.await?;
Ok(data_object(envelope))
}
}
fn project_query(project: Option<&str>) -> Vec<(String, String)> {
match project {
Some(project) => vec![("project".to_string(), project.to_string())],
None => Vec::new(),
}
}
fn string_field(object: &JsonObject, key: &str) -> String {
object
.get(key)
.and_then(Value::as_str)
.unwrap_or("")
.to_string()
}