use std::fmt::Write as _;
use reqwest::{blocking::Client, header, StatusCode};
use crate::error::InternalError;
use crate::rest_api::SPLINTER_PROTOCOL_VERSION;
use super::{AdminServiceClient, CircuitListSlice, CircuitSlice, ProposalListSlice, ProposalSlice};
const PAGING_LIMIT: u32 = 100;
#[derive(Deserialize)]
struct ServerError {
pub message: String,
}
pub struct ReqwestAdminServiceClient {
url: String,
auth: String,
}
impl ReqwestAdminServiceClient {
pub fn new(url: String, auth: String) -> Self {
ReqwestAdminServiceClient { url, auth }
}
}
impl AdminServiceClient for ReqwestAdminServiceClient {
fn submit_admin_payload(&self, payload: Vec<u8>) -> Result<(), InternalError> {
let request = Client::new()
.post(&format!("{}/admin/submit", self.url))
.header(header::CONTENT_TYPE, "octet-stream")
.header("SplinterProtocolVersion", SPLINTER_PROTOCOL_VERSION)
.body(payload)
.header("Authorization", &self.auth);
request
.send()
.map_err(|err| {
InternalError::from_source_with_message(
Box::new(err),
"Failed to submit admin payload".to_string(),
)
})
.and_then(|res| {
let status = res.status();
if status.is_success() {
Ok(())
} else {
let message = res
.json::<ServerError>()
.map_err(|_| {
InternalError::with_message(format!(
"Admin payload submit request failed with status code '{}', but \
error response was not valid",
status
))
})?
.message;
Err(InternalError::with_message(format!(
"Failed to submit admin payload: {}",
message
)))
}
})
}
fn list_circuits(&self, filter: Option<&str>) -> Result<CircuitListSlice, InternalError> {
let mut url = format!("{}/admin/circuits?limit={}", self.url, PAGING_LIMIT);
if let Some(filter) = filter {
if filter.starts_with("status") {
url = format!("{}&{}", &url, &filter);
} else {
url = format!("{}&filter={}", &url, &filter);
}
}
let request = Client::new()
.get(&url)
.header("SplinterProtocolVersion", SPLINTER_PROTOCOL_VERSION)
.header("Authorization", &self.auth);
let response = request.send();
response
.map_err(|err| {
InternalError::from_source_with_message(
Box::new(err),
"Failed to list circuits".to_string(),
)
})
.and_then(|res| {
let status = res.status();
if status.is_success() {
res.json::<CircuitListSlice>().map_err(|_| {
InternalError::with_message(
"Request was successful, but received an invalid response".into(),
)
})
} else {
let message = res
.json::<ServerError>()
.map_err(|err| {
InternalError::from_source_with_message(
err.into(),
format!(
"Circuit list request failed with status code '{}', but error \
response was not valid",
status
),
)
})?
.message;
Err(InternalError::with_message(format!(
"Failed to list circuits: {}",
message
)))
}
})
}
fn fetch_circuit(&self, circuit_id: &str) -> Result<Option<CircuitSlice>, InternalError> {
let request = Client::new()
.get(&format!("{}/admin/circuits/{}", self.url, circuit_id))
.header("SplinterProtocolVersion", SPLINTER_PROTOCOL_VERSION)
.header("Authorization", &self.auth);
request
.send()
.map_err(|err| {
InternalError::from_source_with_message(
Box::new(err),
"Failed to fetch circuit".to_string(),
)
})
.and_then(|res| {
let status = res.status();
if status.is_success() {
res.json::<CircuitSlice>().map(Some).map_err(|_| {
InternalError::with_message(
"Request was successful, but received an invalid response".into(),
)
})
} else if status == StatusCode::NOT_FOUND {
Ok(None)
} else {
let message = res
.json::<ServerError>()
.map_err(|_| {
InternalError::with_message(format!(
"Circuit fetch request failed with status code '{}', but error \
response was not valid",
status
))
})?
.message;
Err(InternalError::with_message(format!(
"Failed to fetch circuit: {}",
message
)))
}
})
}
fn list_proposals(
&self,
management_type_filter: Option<&str>,
member_filter: Option<&str>,
) -> Result<ProposalListSlice, InternalError> {
let mut filters = vec![];
if let Some(management_type) = management_type_filter {
filters.push(format!("management_type={}", management_type));
}
if let Some(member) = member_filter {
filters.push(format!("member={}", member));
}
let mut url = format!("{}/admin/proposals?limit={}", self.url, PAGING_LIMIT);
if !filters.is_empty() {
if let Err(e) = write!(url, "&{}", filters.join("&")) {
return Err(InternalError::from_source(Box::new(e)));
}
}
let request = Client::new()
.get(&url)
.header("SplinterProtocolVersion", SPLINTER_PROTOCOL_VERSION)
.header("Authorization", &self.auth);
request
.send()
.map_err(|err| {
InternalError::from_source_with_message(
Box::new(err),
"Failed to list proposals".to_string(),
)
})
.and_then(|res| {
let status = res.status();
if status.is_success() {
res.json::<ProposalListSlice>().map_err(|_| {
InternalError::with_message(
"Request was successful, but received an invalid response".into(),
)
})
} else {
let message = res
.json::<ServerError>()
.map_err(|_| {
InternalError::with_message(format!(
"Proposal list request failed with status code '{}', but error \
response was not valid",
status
))
})?
.message;
Err(InternalError::with_message(format!(
"Failed to list proposals: {}",
message
)))
}
})
}
fn fetch_proposal(&self, circuit_id: &str) -> Result<Option<ProposalSlice>, InternalError> {
let request = Client::new()
.get(&format!("{}/admin/proposals/{}", self.url, circuit_id))
.header("SplinterProtocolVersion", SPLINTER_PROTOCOL_VERSION)
.header("Authorization", &self.auth);
request
.send()
.map_err(|err| {
InternalError::from_source_with_message(
Box::new(err),
"Failed to fetch proposal".to_string(),
)
})
.and_then(|res| {
let status = res.status();
if status.is_success() {
res.json::<ProposalSlice>().map(Some).map_err(|_| {
InternalError::with_message(
"Request was successful, but received an invalid response".into(),
)
})
} else if status == StatusCode::NOT_FOUND {
Ok(None)
} else {
let message = res
.json::<ServerError>()
.map_err(|_| {
InternalError::with_message(format!(
"Proposal fetch request failed with status code '{}', but error \
response was not valid",
status
))
})?
.message;
Err(InternalError::with_message(format!(
"Failed to fetch proposal: {}",
message
)))
}
})
}
}