use http::response::Response;
use http::uri::Uri;
use http::{Request, StatusCode};
use log::{debug, error, info};
use serde_json::Value;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use thiserror::Error;
use ureq::middleware::MiddlewareNext;
use ureq::{Agent, Body, ResponseExt, SendBody};
use crate::server_models::*;
const CONNECT_TIMEOUT: Duration = Duration::from_millis(800);
pub(crate) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(6);
const EMPTY_STRING: &str = "";
const API_VERSION: &str = "v1";
const JSON_CONTENT_TYPE: &str = "application/json";
#[derive(Debug, Error)]
pub enum IntegresqlError {
#[error("Postgres DB not ready")]
ManagerNotReady,
#[error("template or test database not found")]
NotFound,
#[error("error connecting to IntegreSQL server: {0}")]
NetworkError(String),
#[error("error while setting up template DB: {0}")]
SetupError(String),
#[error("{0}")]
OtherError(String),
}
fn log_request(
req: Request<SendBody>,
next: MiddlewareNext,
) -> Result<Response<Body>, ureq::Error> {
let start = Instant::now();
info!(
"Sending request to IntegreSQL method={} uri={}",
req.method(),
req.uri()
);
let result = next.handle(req);
let duration = start.elapsed();
if duration > Duration::from_secs(1) {
log::warn!(
"Slow response from IntegreSQL response_status={} uri={} duration_seconds={:.2?}",
result.as_ref().map(|r| r.status().as_u16()).unwrap_or(0),
result.as_ref().map(|r| r.get_uri().path()).unwrap_or(""),
duration
);
}
result
}
pub(crate) enum InitializeTemplateResult {
Success(Database),
TemplateAlreadyInitialized,
Err(IntegresqlError),
}
#[derive(Clone)]
pub(crate) struct Client {
base_url: Arc<str>,
http_agent: Agent,
}
impl std::fmt::Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("base_url", &self.base_url)
.finish()
}
}
impl Client {
pub fn new(base_url: Uri, timeout: Duration) -> Self {
let http_agent = Agent::config_builder()
.timeout_connect(Some(CONNECT_TIMEOUT))
.timeout_global(Some(timeout))
.http_status_as_error(false)
.middleware(log_request)
.build()
.new_agent();
Self {
base_url: Arc::from(base_url.to_string()),
http_agent,
}
}
pub fn clear_db_tracking(&self) -> Result<(), IntegresqlError> {
let resp = self.do_delete("admin/templates")?;
match resp.status {
StatusCode::NO_CONTENT => Ok(()),
_ => {
error!("Failed to clear template databases: {}", resp.body);
Err(IntegresqlError::OtherError(resp.body))
}
}
}
pub fn initialize_template(&self, template_hash: TemplateHash) -> InitializeTemplateResult {
let payload = TemplateRequest {
hash: template_hash,
};
let resp = match self.do_post("templates", &payload) {
Ok(resp) => resp,
Err(e) => {
error!("Failed to initialize template database: {}", e);
return InitializeTemplateResult::Err(e);
}
};
match resp.status {
StatusCode::OK => {
let result: Result<InitializeTemplateResponse, IntegresqlError> =
resp.parse_result();
match result {
Ok(result) => InitializeTemplateResult::Success(result.database),
Err(e) => InitializeTemplateResult::Err(e),
}
}
StatusCode::LOCKED => {
debug!(
"Skipping template setup, template {} is already initialized",
template_hash
);
InitializeTemplateResult::TemplateAlreadyInitialized
}
_ => InitializeTemplateResult::Err(resp.into_error()),
}
}
pub fn finalize_template(&self, template_hash: TemplateHash) -> Result<(), IntegresqlError> {
let endpoint = format!("templates/{}", template_hash);
self.do_put(&endpoint, &Value::Null)?.into_empty_result()
}
pub fn discard_template(&self, template_hash: TemplateHash) -> Result<(), IntegresqlError> {
let endpoint = format!("templates/{}", template_hash);
let result = self.do_delete(&endpoint)?;
match result.status {
StatusCode::NO_CONTENT | StatusCode::NOT_FOUND => Ok(()),
_ => Err(result.into_error()),
}
}
pub fn get_test_db(
&self,
template_hash: TemplateHash,
) -> Result<GetTestDbResponse, IntegresqlError> {
let endpoint = format!("templates/{}/tests", template_hash);
self.do_get(&endpoint)?.parse_result()
}
pub fn recreate_test_db(
&self,
template_hash: TemplateHash,
id: i32,
) -> Result<(), IntegresqlError> {
let endpoint = format!("templates/{}/tests/{}/recreate", template_hash, id);
self.do_post(&endpoint, &EMPTY_STRING)?.into_empty_result()
}
pub fn unlock_test_db(
&self,
template_hash: TemplateHash,
id: i32,
) -> Result<(), IntegresqlError> {
let endpoint = format!("templates/{}/tests/{}/unlock", template_hash, id);
self.do_post(&endpoint, &EMPTY_STRING)?.into_empty_result()
}
fn do_get(&self, endpoint: &str) -> Result<ServerResponse, IntegresqlError> {
self.http_agent
.get(self.get_full_endpoint(endpoint))
.header("Accept", JSON_CONTENT_TYPE)
.call()
.map_err(|e| IntegresqlError::NetworkError(e.to_string()))
.map(|r| r.into())
}
fn do_post<T>(&self, endpoint: &str, body: &T) -> Result<ServerResponse, IntegresqlError>
where
T: serde::Serialize,
{
self.http_agent
.post(self.get_full_endpoint(endpoint))
.header("Content-Type", JSON_CONTENT_TYPE)
.header("Accept", JSON_CONTENT_TYPE)
.send_json(body)
.map_err(|e| IntegresqlError::NetworkError(e.to_string()))
.map(|r| r.into())
}
fn do_put<T>(&self, endpoint: &str, body: &T) -> Result<ServerResponse, IntegresqlError>
where
T: serde::Serialize,
{
self.http_agent
.put(self.get_full_endpoint(endpoint))
.header("Content-Type", JSON_CONTENT_TYPE)
.header("Accept", JSON_CONTENT_TYPE)
.send_json(body)
.map_err(|e| IntegresqlError::NetworkError(e.to_string()))
.map(|r| r.into())
}
fn do_delete(&self, endpoint: &str) -> Result<ServerResponse, IntegresqlError> {
self.http_agent
.delete(self.get_full_endpoint(endpoint))
.header("Accept", JSON_CONTENT_TYPE)
.call()
.map_err(|e| IntegresqlError::NetworkError(e.to_string()))
.map(|r| r.into())
}
fn get_full_endpoint(&self, endpoint: &str) -> String {
let full_url = format!("{}/{}/{}", self.base_url, API_VERSION, endpoint);
full_url
}
}
struct ServerResponse {
status: StatusCode,
body: String,
}
impl From<Response<Body>> for ServerResponse {
fn from(val: Response<Body>) -> Self {
debug!("Received response from IntegreSQL: {}", val.status());
ServerResponse {
status: val.status(),
body: val.into_body().read_to_string().unwrap_or_default(),
}
}
}
impl ServerResponse {
fn into_error(self) -> IntegresqlError {
match self.status {
StatusCode::NOT_FOUND => {
error!("Got 404 response from IntegreSQL: {}", self.body);
IntegresqlError::NotFound
}
StatusCode::SERVICE_UNAVAILABLE => {
error!("Got 503 response from IntegreSQL: {}", self.body);
IntegresqlError::ManagerNotReady
}
_ => {
error!(
"Got unexpected HTTP response from IntegreSQL: {} - {}",
self.status, self.body
);
IntegresqlError::OtherError(self.body)
}
}
}
fn into_empty_result(self) -> Result<(), IntegresqlError> {
match self.status {
StatusCode::NO_CONTENT => Ok(()),
_ => Err(self.into_error()),
}
}
fn parse_result<T>(self) -> Result<T, IntegresqlError>
where
T: serde::de::DeserializeOwned,
{
match self.status {
StatusCode::OK => {
let parsed: T = serde_json::from_str(&self.body).map_err(|e| {
IntegresqlError::NetworkError(format!("failed to parse response JSON: {}", e))
})?;
Ok(parsed)
}
_ => Err(self.into_error()),
}
}
}