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
17pub 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 #[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 #[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 #[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 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 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}