use std::sync::Arc;
use std::time::Duration;
use dvm_api::{ApiError, SandboxId};
use reqwest::{Method, RequestBuilder, Response};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::credentials;
use crate::error::{Error, Result};
use crate::execution::Execution;
use crate::sandbox::{Sandbox, SandboxBuilder};
pub const DEFAULT_API_URL: &str = "https://sandbox.dvm.run";
const QUIET_LIMIT: Duration = Duration::from_secs(60);
#[derive(Clone, Debug)]
pub struct Client {
inner: Arc<Connection>,
}
#[derive(Debug)]
struct Connection {
url: String,
api: String,
key: Option<String>,
http: reqwest::Client,
}
pub(crate) type Query<'a> = &'a [(&'a str, String)];
impl Client {
pub fn from_env() -> Result<Self> {
let api_url = api_url_of(None);
let key = match std::env::var("DVM_API_KEY") {
Ok(key) if !key.is_empty() => Some(key),
_ => credentials::saved_key(&api_url)?,
};
let key = key.ok_or_else(|| Error::NoApiKey {
api_url: api_url.clone(),
})?;
Ok(Self::new(api_url, key))
}
pub fn new(api_url: impl Into<String>, api_key: impl Into<String>) -> Self {
Self::with_key(api_url.into(), Some(api_key.into()))
}
pub(crate) fn with_key(api_url: String, key: Option<String>) -> Self {
let url = api_url.trim_end_matches('/').to_owned();
Self {
inner: Arc::new(Connection {
api: format!("{url}/api"),
url,
key,
http: reqwest::Client::new(),
}),
}
}
pub fn api_url(&self) -> &str {
&self.inner.url
}
pub fn create_sandbox(&self, image: impl Into<String>) -> SandboxBuilder {
SandboxBuilder::new(self.clone(), image.into())
}
pub async fn connect_sandbox(&self, id: SandboxId) -> Result<Sandbox> {
let sandbox = self
.json(Method::GET, &format!("/sandboxes/{id}"), &[], None::<&()>)
.await?;
Ok(Sandbox::new(self.clone(), sandbox))
}
pub async fn execution(&self, id: SandboxId) -> Result<Execution> {
let execution = self
.json(Method::GET, &format!("/executions/{id}"), &[], None::<&()>)
.await?;
Ok(Execution::new(self.clone(), execution))
}
pub(crate) async fn json<T: DeserializeOwned>(
&self,
method: Method,
path: &str,
query: Query<'_>,
body: Option<&impl Serialize>,
) -> Result<T> {
let mut request = self.request(method, path, query).timeout(QUIET_LIMIT);
if let Some(body) = body {
request = request.json(body);
}
let bytes = self.send(path, request).await?.bytes().await;
let bytes = bytes.map_err(|error| Error::network(format!("reading {path}"), error))?;
let bytes = if bytes.is_empty() {
&b"null"[..]
} else {
&bytes
};
serde_json::from_slice(bytes)
.map_err(|error| Error::Unexpected(format!("{path}'s answer: {error}")))
}
pub(crate) async fn bytes(
&self,
method: Method,
path: &str,
query: Query<'_>,
content: Option<Vec<u8>>,
) -> Result<Vec<u8>> {
let mut request = self.request(method, path, query);
if let Some(content) = content {
request = request.body(content);
}
let bytes = self.send(path, request).await?.bytes().await;
Ok(bytes
.map_err(|error| Error::network(format!("reading {path}"), error))?
.to_vec())
}
pub(crate) async fn events(&self, path: &str, query: Query<'_>) -> Result<EventStream> {
let request = self.request(Method::GET, path, query);
let answered = tokio::time::timeout(QUIET_LIMIT, self.send(path, request)).await;
let response = answered.map_err(|_| Error::Network {
context: format!("{path} didn't answer"),
source: None,
})??;
Ok(EventStream {
path: path.to_owned(),
response,
pending: Vec::new(),
data: Vec::new(),
})
}
fn request(&self, method: Method, path: &str, query: Query<'_>) -> RequestBuilder {
let request = self
.inner
.http
.request(method, format!("{}{path}", self.inner.api));
let request = if query.is_empty() {
request
} else {
request.query(query)
};
match &self.inner.key {
Some(key) => request.bearer_auth(key),
None => request,
}
}
async fn send(&self, path: &str, request: RequestBuilder) -> Result<Response> {
let response = request.send().await.map_err(|error| {
Error::network(format!("couldn't reach {}{path}", self.inner.api), error)
})?;
let status = response.status();
if status.is_success() {
return Ok(response);
}
let text = response.text().await.unwrap_or_default();
let reason = match serde_json::from_str::<ApiError>(&text) {
Ok(refusal) => refusal.message,
Err(_) if text.is_empty() => status.canonical_reason().unwrap_or("").to_owned(),
Err(_) => text,
};
Err(Error::refusal(status.as_u16(), reason))
}
}
pub(crate) struct EventStream {
path: String,
response: Response,
pending: Vec<u8>,
data: Vec<u8>,
}
impl EventStream {
pub(crate) async fn next<T: DeserializeOwned>(&mut self) -> Result<Option<T>> {
loop {
while let Some(end) = self.pending.iter().position(|&byte| byte == b'\n') {
let mut line: Vec<u8> = self.pending.drain(..=end).collect();
line.pop();
if line.last() == Some(&b'\r') {
line.pop();
}
if let Some(data) = line.strip_prefix(b"data:") {
if !self.data.is_empty() {
self.data.push(b'\n');
}
self.data
.extend_from_slice(data.strip_prefix(b" ").unwrap_or(data));
} else if line.is_empty() && !self.data.is_empty() {
let data = std::mem::take(&mut self.data);
let event = serde_json::from_slice(&data).map_err(|error| {
Error::Unexpected(format!("{}'s event: {error}", self.path))
})?;
return Ok(Some(event));
}
}
let chunk = tokio::time::timeout(QUIET_LIMIT, self.response.chunk()).await;
let chunk = chunk.map_err(|_| Error::Network {
context: format!("{}'s stream went quiet", self.path),
source: None,
})?;
match chunk.map_err(|error| Error::network(format!("{}'s stream", self.path), error))? {
Some(chunk) => self.pending.extend_from_slice(&chunk),
None => return Ok(None),
}
}
}
}
pub(crate) fn api_url_of(given: Option<&str>) -> String {
let from_environment = std::env::var("DVM_API_URL").ok();
let url = given
.filter(|url| !url.is_empty())
.or(from_environment.as_deref().filter(|url| !url.is_empty()))
.unwrap_or(DEFAULT_API_URL);
url.trim_end_matches('/').to_owned()
}