use bytes::Bytes;
use http::header::{AUTHORIZATION, CONTENT_TYPE, HeaderName};
use http::{HeaderMap, HeaderValue, Method, StatusCode};
use serde::Serialize;
use serde::de::DeserializeOwned;
use sofie_core::encoding::percent;
use sofie_core::{Error, Result};
use sofie_db::IdempotencyKey;
use crate::Outgoing;
use crate::exchange::Exchange;
use crate::guard::Reach;
use crate::target::Target;
const IDEMPOTENCY_KEY: HeaderName = HeaderName::from_static("idempotency-key");
pub struct Request<'a> {
outgoing: &'a Outgoing,
method: Method,
target: Result<Target>,
reach: Reach,
headers: HeaderMap,
body: Bytes,
keyed: bool,
problem: Option<Error>,
}
impl<'a> Request<'a> {
pub(crate) fn new(
outgoing: &'a Outgoing,
method: Method,
target: Result<Target>,
reach: Reach,
) -> Self {
Self {
outgoing,
method,
target,
reach,
headers: HeaderMap::new(),
body: Bytes::new(),
keyed: false,
problem: None,
}
}
#[must_use]
pub fn header(mut self, name: &str, value: &str) -> Self {
let Ok(header) = HeaderName::from_bytes(name.as_bytes()) else {
self.fail(&format!("`{name}` is not a valid header name"));
return self;
};
match HeaderValue::from_str(value) {
Ok(mut value) => {
value.set_sensitive(true);
self.headers.insert(header, value);
}
Err(_) => self.fail(&format!("the value of header `{name}` is not valid")),
}
self
}
#[must_use]
pub fn bearer(self, token: &str) -> Self {
self.header(AUTHORIZATION.as_str(), &format!("Bearer {token}"))
}
#[must_use]
pub fn json<T: Serialize + ?Sized>(mut self, body: &T) -> Self {
match serde_json::to_vec(body) {
Ok(json) => self.body("application/json", json),
Err(error) => {
self.fail(&format!("serializing the body failed: {error}"));
self
}
}
}
#[must_use]
pub fn form(self, fields: &[(&str, &str)]) -> Self {
let body = percent::form(fields.iter().copied());
self.body("application/x-www-form-urlencoded", body)
}
#[must_use]
pub fn body(mut self, content_type: &str, body: impl Into<Bytes>) -> Self {
match HeaderValue::from_str(content_type) {
Ok(value) => {
self.body = body.into();
self.headers.insert(CONTENT_TYPE, value);
}
Err(_) => self.fail(&format!("`{content_type}` is not a valid content type")),
}
self
}
#[must_use]
pub fn idempotency(mut self, key: &IdempotencyKey) -> Self {
match HeaderValue::from_str(key.as_str()) {
Ok(value) => {
self.headers.insert(IDEMPOTENCY_KEY, value);
self.keyed = true;
}
Err(_) => self.fail("the idempotency key is not a valid header value"),
}
self
}
#[must_use]
pub fn repeatable(mut self) -> Self {
self.keyed = true;
self
}
pub async fn send(self) -> Result<Response> {
if let Some(problem) = self.problem {
return Err(problem);
}
let target = self.target?;
sofie_db::read_untracked();
let exchange = Exchange {
shared: &self.outgoing.shared,
method: self.method,
reach: self.reach,
headers: self.headers,
body: self.body,
keyed: self.keyed,
};
Box::pin(exchange.send(target)).await
}
fn fail(&mut self, message: &str) {
self.problem.get_or_insert_with(|| {
Error::internal_message(format!(
"building a request to {}: {message}",
self.outgoing.shared.vendor
))
});
}
}
#[derive(Debug)]
pub struct Response {
pub(crate) vendor: &'static str,
pub(crate) status: StatusCode,
pub(crate) headers: HeaderMap,
pub(crate) body: Bytes,
}
impl Response {
#[must_use]
pub fn status(&self) -> u16 {
self.status.as_u16()
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.get(name)?.to_str().ok()
}
#[must_use]
pub fn body(&self) -> &[u8] {
&self.body
}
#[must_use]
pub fn into_body(self) -> Bytes {
self.body
}
pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
if !self.status.is_success() {
return Err(Error::internal_message(format!(
"{} answered {}",
self.vendor, self.status
)));
}
serde_json::from_slice(&self.body).map_err(|error| {
Error::internal(format!("reading {}'s answer as JSON", self.vendor), error)
})
}
}