use serde::Deserialize;
use std::collections::HashMap;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("elastik {status}: {body}")]
Elastik { status: u16, body: String },
#[error("invalid header: {0}")]
Header(String),
#[error("JSON: {0}")]
Json(#[from] serde_json::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Deserialize)]
pub struct Envelope {
pub stage_html: String,
pub version: i64,
pub ext: String,
pub updated_at: String,
#[serde(default)]
pub headers: serde_json::Value,
}
#[derive(Debug, Deserialize)]
pub struct PutResponse {
pub ok: bool,
pub version: i64,
pub world: String,
pub size: u64,
}
#[derive(Debug, Deserialize)]
struct WorldEntry {
name: String,
}
pub struct Elastik {
url: String,
token: String,
client: reqwest::blocking::Client,
}
impl Elastik {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into().trim_end_matches('/').to_owned(),
token: String::new(),
client: reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("build client"),
}
}
pub fn token(mut self, t: impl Into<String>) -> Self {
self.token = t.into();
self
}
pub fn put(&self, path: &str, data: &[u8], meta: &[(&str, &str)]) -> Result<PutResponse> {
let mut req = self.client.put(self.full(path)).body(data.to_vec());
req = self.auth(req);
for (k, v) in meta {
let name = format!("X-Meta-{}", k.replace('_', "-"));
req = req.header(&name, *v);
}
let r = req.send()?;
let status = r.status().as_u16();
let text = r.text()?;
if status >= 400 {
return Err(Error::Elastik { status, body: text });
}
Ok(serde_json::from_str(&text)?)
}
pub fn get(&self, path: &str) -> Result<Envelope> {
let r = self.auth(self.client.get(self.full(path))).send()?;
let status = r.status().as_u16();
let text = r.text()?;
if status >= 400 {
return Err(Error::Elastik { status, body: text });
}
Ok(serde_json::from_str(&text)?)
}
pub fn get_raw(&self, path: &str) -> Result<Vec<u8>> {
let r = self.auth(self.client.get(self.full(path) + "?raw")).send()?;
let status = r.status().as_u16();
if status >= 400 {
return Err(Error::Elastik { status, body: r.text().unwrap_or_default() });
}
Ok(r.bytes()?.to_vec())
}
pub fn head(&self, path: &str) -> Result<HashMap<String, String>> {
let r = self.auth(self.client.head(self.full(path))).send()?;
if !r.status().is_success() {
return Err(Error::Elastik {
status: r.status().as_u16(),
body: String::new(),
});
}
let mut out = HashMap::new();
for (k, v) in r.headers().iter() {
if let Ok(s) = v.to_str() {
out.insert(k.as_str().to_lowercase(), s.to_owned());
}
}
Ok(out)
}
pub fn delete(&self, path: &str) -> Result<bool> {
let r = self.auth(self.client.delete(self.full(path))).send()?;
match r.status().as_u16() {
204 => Ok(true),
404 => Ok(false),
s => Err(Error::Elastik {
status: s,
body: r.text().unwrap_or_default(),
}),
}
}
pub fn list(&self) -> Result<Vec<String>> {
let r = self.auth(self.client.get(self.full("/proc/worlds"))).send()?;
if !r.status().is_success() {
return Err(Error::Elastik {
status: r.status().as_u16(),
body: r.text().unwrap_or_default(),
});
}
let entries: Vec<WorldEntry> = r.json()?;
Ok(entries.into_iter().map(|e| e.name).collect())
}
pub fn shaped(&self, path: &str, accept: &str, intent: &str) -> Result<Vec<u8>> {
let url = self.url.clone() + "/shaped" + (if path.starts_with('/') { path } else { "/" }) + path;
let mut req = self.client.get(&url).header("Accept", accept);
req = self.auth(req);
if !intent.is_empty() {
req = req.header("X-Semantic-Intent", intent);
}
let r = req.send()?;
if !r.status().is_success() {
return Err(Error::Elastik {
status: r.status().as_u16(),
body: r.text().unwrap_or_default(),
});
}
Ok(r.bytes()?.to_vec())
}
fn full(&self, path: &str) -> String {
if path.starts_with('/') {
self.url.clone() + path
} else {
self.url.clone() + "/" + path
}
}
fn auth(&self, req: reqwest::blocking::RequestBuilder) -> reqwest::blocking::RequestBuilder {
if self.token.is_empty() {
req
} else {
req.header("Authorization", format!("Bearer {}", self.token))
}
}
}