use std::time::Duration;
use ureq::http::HeaderMap;
use ytsaurus_yson::{YsonFormat, YsonValue, to_string};
use crate::error::{ClientError, Result, truncate};
const HEADER_FORMAT: &str = "X-YT-Header-Format";
const PARAMETERS: &str = "X-YT-Parameters";
const ERROR: &str = "X-YT-Error";
macro_rules! with_headers {
($request:expr, $headers:expr) => {{
let mut request = $request;
for (name, value) in $headers {
request = request.header(*name, value.as_str());
}
request
}};
}
pub(crate) enum Payload<'a> {
None,
Bytes(&'a [u8]),
}
#[derive(Clone)]
pub(crate) struct Transport {
agent: ureq::Agent,
base: String,
token: Option<String>,
}
impl std::fmt::Debug for Transport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Transport")
.field("base", &self.base)
.field("token", &self.token.as_ref().map(|_| "<redacted>"))
.finish()
}
}
impl Transport {
pub(crate) fn new(proxy: &str, token: Option<String>, timeout: Duration) -> Self {
let base = if proxy.starts_with("http://") || proxy.starts_with("https://") {
proxy.trim_end_matches('/').to_owned()
} else {
format!("https://{}", proxy.trim_end_matches('/'))
};
let agent: ureq::Agent = ureq::Agent::config_builder()
.timeout_global(Some(timeout))
.http_status_as_error(false)
.build()
.into();
Self { agent, base, token }
}
pub(crate) fn base(&self) -> &str {
&self.base
}
pub(crate) fn call(
&self,
method: Method,
command: &str,
parameters: &YsonValue,
payload: Payload<'_>,
) -> Result<Vec<u8>> {
let url = format!("{}/api/v4/{command}", self.base);
let encoded = to_string(parameters, YsonFormat::Text).map_err(|e| ClientError::Decode {
command: command.to_owned(),
reason: format!("could not encode parameters: {e}"),
})?;
let mut headers: Vec<(&str, String)> = vec![
(HEADER_FORMAT, "<format=text>yson".to_owned()),
(PARAMETERS, encoded),
("X-YT-Output-Format", "<format=text>yson".to_owned()),
];
if let Some(token) = &self.token {
headers.push(("Authorization", format!("OAuth {token}")));
}
let sent = match (method, payload) {
(Method::Get, _) => with_headers!(self.agent.get(&url), &headers).call(),
(Method::Post, Payload::None) => {
with_headers!(self.agent.post(&url), &headers).send_empty()
}
(Method::Post, Payload::Bytes(bytes)) => with_headers!(self.agent.post(&url), &headers)
.header("Content-Type", "application/octet-stream")
.send(bytes),
(Method::Put, Payload::None) => {
with_headers!(self.agent.put(&url), &headers).send_empty()
}
(Method::Put, Payload::Bytes(bytes)) => with_headers!(self.agent.put(&url), &headers)
.header("Content-Type", "application/octet-stream")
.send(bytes),
};
let mut response = sent.map_err(|e| ClientError::Transport {
command: command.to_owned(),
source: Box::new(e),
})?;
let status = response.status().as_u16();
if let Some(raw) = header_value(response.headers(), ERROR) {
return Err(ClientError::from_yt_error(command, status, &raw));
}
let body = response
.body_mut()
.with_config()
.limit(512 * 1024 * 1024)
.read_to_vec()
.map_err(|e| ClientError::Decode {
command: command.to_owned(),
reason: format!("could not read the response body: {e}"),
})?;
if !(200..300).contains(&status) {
return Err(ClientError::Http {
command: command.to_owned(),
status,
body: truncate(&String::from_utf8_lossy(&body), 400),
});
}
Ok(body)
}
}
#[derive(Clone, Copy)]
pub(crate) enum Method {
Get,
Post,
Put,
}
fn header_value(headers: &HeaderMap, name: &str) -> Option<String> {
headers
.get(name)
.and_then(|value| value.to_str().ok())
.map(str::to_owned)
}