Skip to main content

sofie_client/
request.rs

1use bytes::Bytes;
2use http::header::{AUTHORIZATION, CONTENT_TYPE, HeaderName};
3use http::{HeaderMap, HeaderValue, Method, StatusCode};
4use serde::Serialize;
5use serde::de::DeserializeOwned;
6use sofie_core::encoding::percent;
7use sofie_core::{Error, Result};
8use sofie_db::IdempotencyKey;
9
10use crate::Outgoing;
11use crate::exchange::Exchange;
12use crate::guard::Reach;
13use crate::target::Target;
14
15const IDEMPOTENCY_KEY: HeaderName = HeaderName::from_static("idempotency-key");
16
17/// A mistake while building, such as an invalid header, is returned by [`Request::send`].
18pub struct Request<'a> {
19    outgoing: &'a Outgoing,
20    method: Method,
21    target: Result<Target>,
22    reach: Reach,
23    headers: HeaderMap,
24    body: Bytes,
25    keyed: bool,
26    problem: Option<Error>,
27}
28
29impl<'a> Request<'a> {
30    pub(crate) fn new(
31        outgoing: &'a Outgoing,
32        method: Method,
33        target: Result<Target>,
34        reach: Reach,
35    ) -> Self {
36        Self {
37            outgoing,
38            method,
39            target,
40            reach,
41            headers: HeaderMap::new(),
42            body: Bytes::new(),
43            keyed: false,
44            problem: None,
45        }
46    }
47
48    /// Header values are never recorded in spans or logs.
49    #[must_use]
50    pub fn header(mut self, name: &str, value: &str) -> Self {
51        let Ok(header) = HeaderName::from_bytes(name.as_bytes()) else {
52            self.fail(&format!("`{name}` is not a valid header name"));
53            return self;
54        };
55        match HeaderValue::from_str(value) {
56            Ok(mut value) => {
57                value.set_sensitive(true);
58                self.headers.insert(header, value);
59            }
60            Err(_) => self.fail(&format!("the value of header `{name}` is not valid")),
61        }
62        self
63    }
64
65    #[must_use]
66    pub fn bearer(self, token: &str) -> Self {
67        self.header(AUTHORIZATION.as_str(), &format!("Bearer {token}"))
68    }
69
70    #[must_use]
71    pub fn json<T: Serialize + ?Sized>(mut self, body: &T) -> Self {
72        match serde_json::to_vec(body) {
73            Ok(json) => self.body("application/json", json),
74            Err(error) => {
75                self.fail(&format!("serializing the body failed: {error}"));
76                self
77            }
78        }
79    }
80
81    /// `application/x-www-form-urlencoded`, as some vendors' APIs take it.
82    #[must_use]
83    pub fn form(self, fields: &[(&str, &str)]) -> Self {
84        let body = percent::form(fields.iter().copied());
85        self.body("application/x-www-form-urlencoded", body)
86    }
87
88    #[must_use]
89    pub fn body(mut self, content_type: &str, body: impl Into<Bytes>) -> Self {
90        match HeaderValue::from_str(content_type) {
91            Ok(value) => {
92                self.body = body.into();
93                self.headers.insert(CONTENT_TYPE, value);
94            }
95            Err(_) => self.fail(&format!("`{content_type}` is not a valid content type")),
96        }
97        self
98    }
99
100    #[must_use]
101    pub fn idempotency(mut self, key: &IdempotencyKey) -> Self {
102        match HeaderValue::from_str(key.as_str()) {
103            Ok(value) => {
104                self.headers.insert(IDEMPOTENCY_KEY, value);
105                self.keyed = true;
106            }
107            Err(_) => self.fail("the idempotency key is not a valid header value"),
108        }
109        self
110    }
111
112    /// A `POST` the vendor applies once however often it arrives, such as a search, is retried
113    /// as a `GET` would be.
114    #[must_use]
115    pub fn repeatable(mut self) -> Self {
116        self.keyed = true;
117        self
118    }
119
120    pub async fn send(self) -> Result<Response> {
121        if let Some(problem) = self.problem {
122            return Err(problem);
123        }
124        let target = self.target?;
125        sofie_db::read_untracked();
126        let exchange = Exchange {
127            shared: &self.outgoing.shared,
128            method: self.method,
129            reach: self.reach,
130            headers: self.headers,
131            body: self.body,
132            keyed: self.keyed,
133        };
134        // Boxed: the connection's futures would otherwise make every caller's future kilobytes larger.
135        Box::pin(exchange.send(target)).await
136    }
137
138    fn fail(&mut self, message: &str) {
139        self.problem.get_or_insert_with(|| {
140            Error::internal_message(format!(
141                "building a request to {}: {message}",
142                self.outgoing.shared.vendor
143            ))
144        });
145    }
146}
147
148#[derive(Debug)]
149pub struct Response {
150    pub(crate) vendor: &'static str,
151    pub(crate) status: StatusCode,
152    pub(crate) headers: HeaderMap,
153    pub(crate) body: Bytes,
154}
155
156impl Response {
157    #[must_use]
158    pub fn status(&self) -> u16 {
159        self.status.as_u16()
160    }
161
162    #[must_use]
163    pub fn header(&self, name: &str) -> Option<&str> {
164        self.headers.get(name)?.to_str().ok()
165    }
166
167    #[must_use]
168    pub fn body(&self) -> &[u8] {
169        &self.body
170    }
171
172    #[must_use]
173    pub fn into_body(self) -> Bytes {
174        self.body
175    }
176
177    /// The body is never put in the error: it may hold what a log must not.
178    pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
179        if !self.status.is_success() {
180            return Err(Error::internal_message(format!(
181                "{} answered {}",
182                self.vendor, self.status
183            )));
184        }
185        serde_json::from_slice(&self.body).map_err(|error| {
186            Error::internal(format!("reading {}'s answer as JSON", self.vendor), error)
187        })
188    }
189}