use serde_json::Value;
use crate::{DecodeSnafu, Error, HttpSnafu, IndexNotFoundSnafu, Result};
use snafu::ResultExt;
#[derive(Clone, Debug)]
pub struct OpenSearchClient {
base_url: String,
http: reqwest::Client,
}
impl OpenSearchClient {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
http: reqwest::Client::new(),
}
}
pub fn with_client(mut self, http: reqwest::Client) -> Self {
self.http = http;
self
}
pub fn base_url(&self) -> &str {
self.base_url.trim_end_matches('/')
}
pub async fn search(&self, index: &str, body: &Value) -> Result<Vec<Value>> {
let url = format!("{}/{index}/_search", self.base_url());
let resp = self
.http
.post(&url)
.json(body)
.send()
.await
.context(HttpSnafu { url: url.clone() })?;
if resp.status().as_u16() == 404 {
return Ok(Vec::new());
}
let resp = check_status(resp, &url).await?;
let doc: Value = resp.json().await.context(DecodeSnafu { url })?;
Ok(doc["hits"]["hits"]
.as_array()
.map(|hits| hits.iter().map(|h| h["_source"].clone()).collect())
.unwrap_or_default())
}
pub async fn mapping(&self, index: &str) -> Result<Value> {
let url = format!("{}/{index}/_mapping", self.base_url());
let resp = self
.http
.get(&url)
.send()
.await
.context(HttpSnafu { url: url.clone() })?;
if resp.status().as_u16() == 404 {
return IndexNotFoundSnafu { index }.fail();
}
let resp = check_status(resp, &url).await?;
resp.json().await.context(DecodeSnafu { url })
}
}
async fn check_status(resp: reqwest::Response, url: &str) -> Result<reqwest::Response> {
if resp.status().is_success() {
return Ok(resp);
}
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
Err(Error::Status {
status,
url: url.to_string(),
body,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base_url_drops_a_trailing_slash() {
assert_eq!(OpenSearchClient::new("http://x:9200/").base_url(), "http://x:9200");
assert_eq!(OpenSearchClient::new("http://x:9200").base_url(), "http://x:9200");
}
}