1use std::fmt;
2
3use bytes::Bytes;
4use reqwest::{
5 header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE},
6 Method, Url,
7};
8use serde::Serialize;
9
10use crate::error::{Error, Result};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Destination {
15 Url(Url),
17 UrlGroup(String),
19}
20
21impl Destination {
22 pub fn url(url: impl AsRef<str>) -> Result<Self> {
24 Url::parse(url.as_ref())
25 .map(Self::Url)
26 .map_err(|error| Error::Config {
27 message: format!("invalid destination url: {error}"),
28 })
29 }
30
31 pub fn url_group(name: impl Into<String>) -> Self {
33 Self::UrlGroup(name.into())
34 }
35
36 pub(crate) fn path_value(&self) -> String {
37 match self {
38 Self::Url(url) => url.as_str().to_owned(),
39 Self::UrlGroup(name) => name.clone(),
40 }
41 }
42}
43
44impl fmt::Display for Destination {
45 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Self::Url(url) => formatter.write_str(url.as_str()),
48 Self::UrlGroup(name) => formatter.write_str(name),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
55pub struct Redaction {
56 body: bool,
57 all_headers: bool,
58 header_names: Vec<String>,
59}
60
61impl Redaction {
62 pub fn new() -> Self {
64 Self::default()
65 }
66
67 pub fn body(mut self) -> Self {
69 self.body = true;
70 self
71 }
72
73 pub fn all_headers(mut self) -> Self {
75 self.all_headers = true;
76 self
77 }
78
79 pub fn header(mut self, name: impl Into<String>) -> Self {
81 self.header_names.push(name.into());
82 self
83 }
84
85 pub(crate) fn header_value(&self) -> Option<String> {
86 let mut parts = Vec::new();
87
88 if self.body {
89 parts.push(String::from("body"));
90 }
91
92 if self.all_headers {
93 parts.push(String::from("header"));
94 }
95
96 parts.extend(
97 self.header_names
98 .iter()
99 .map(|name| format!("header[{name}]")),
100 );
101
102 if parts.is_empty() {
103 None
104 } else {
105 Some(parts.join(","))
106 }
107 }
108}
109
110#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)]
112#[serde(rename_all = "camelCase")]
113pub struct PublishedMessage {
114 pub message_id: Option<String>,
116 pub url: Option<String>,
118 pub error: Option<String>,
120 pub deduplicated: Option<bool>,
122}
123
124pub type PublishResponse = Vec<PublishedMessage>;
126
127#[derive(Debug, Clone)]
129pub struct PublishRequest {
130 pub(crate) destination: Destination,
131 pub(crate) body: Option<Bytes>,
132 pub(crate) headers: HeaderMap,
133 pub(crate) method: Method,
134 pub(crate) delay: Option<u32>,
135 pub(crate) not_before: Option<u64>,
136 pub(crate) deduplication_id: Option<String>,
137 pub(crate) content_based_deduplication: bool,
138 pub(crate) retries: Option<u32>,
139 pub(crate) retry_delay: Option<String>,
140 pub(crate) callback: Option<String>,
141 pub(crate) failure_callback: Option<String>,
142 pub(crate) timeout: Option<u32>,
143 pub(crate) label: Option<String>,
144 pub(crate) redaction: Option<Redaction>,
145}
146
147impl PublishRequest {
148 pub fn builder(destination: Destination) -> PublishRequestBuilder {
150 PublishRequestBuilder::new(destination)
151 }
152
153 pub fn destination(&self) -> &Destination {
155 &self.destination
156 }
157}
158
159#[derive(Debug, Clone)]
161pub struct PublishRequestBuilder {
162 request: PublishRequest,
163}
164
165impl PublishRequestBuilder {
166 fn new(destination: Destination) -> Self {
167 Self {
168 request: PublishRequest {
169 destination,
170 body: None,
171 headers: HeaderMap::new(),
172 method: Method::POST,
173 delay: None,
174 not_before: None,
175 deduplication_id: None,
176 content_based_deduplication: false,
177 retries: None,
178 retry_delay: None,
179 callback: None,
180 failure_callback: None,
181 timeout: None,
182 label: None,
183 redaction: None,
184 },
185 }
186 }
187
188 pub fn body(mut self, body: impl Into<Bytes>) -> Self {
190 self.request.body = Some(body.into());
191 self
192 }
193
194 pub fn json_body<T>(mut self, body: &T) -> Result<Self>
196 where
197 T: Serialize,
198 {
199 self.request.body = Some(Bytes::from(
200 serde_json::to_vec(body).map_err(Error::Serialize)?,
201 ));
202 self.request
203 .headers
204 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
205 Ok(self)
206 }
207
208 pub fn header(mut self, name: &str, value: &str) -> Result<Self> {
210 let header_name =
211 HeaderName::from_bytes(name.as_bytes()).map_err(|error| Error::InvalidRequest {
212 message: format!("invalid header name `{name}`: {error}"),
213 })?;
214 let header_value = HeaderValue::from_str(value).map_err(|error| Error::InvalidRequest {
215 message: format!("invalid header value for `{name}`: {error}"),
216 })?;
217 self.request.headers.insert(header_name, header_value);
218 Ok(self)
219 }
220
221 pub fn headers(mut self, headers: HeaderMap) -> Self {
223 self.request.headers = headers;
224 self
225 }
226
227 pub fn method(mut self, method: Method) -> Self {
229 self.request.method = method;
230 self
231 }
232
233 pub fn delay_seconds(mut self, seconds: u32) -> Self {
235 self.request.delay = Some(seconds);
236 self
237 }
238
239 pub fn not_before(mut self, timestamp: u64) -> Self {
241 self.request.not_before = Some(timestamp);
242 self
243 }
244
245 pub fn deduplication_id(mut self, id: impl Into<String>) -> Self {
247 self.request.deduplication_id = Some(id.into());
248 self
249 }
250
251 pub fn content_based_deduplication(mut self, enabled: bool) -> Self {
253 self.request.content_based_deduplication = enabled;
254 self
255 }
256
257 pub fn retries(mut self, retries: u32) -> Self {
259 self.request.retries = Some(retries);
260 self
261 }
262
263 pub fn retry_delay(mut self, expression: impl Into<String>) -> Self {
265 self.request.retry_delay = Some(expression.into());
266 self
267 }
268
269 pub fn callback(mut self, url: impl Into<String>) -> Self {
271 self.request.callback = Some(url.into());
272 self
273 }
274
275 pub fn failure_callback(mut self, url: impl Into<String>) -> Self {
277 self.request.failure_callback = Some(url.into());
278 self
279 }
280
281 pub fn timeout_seconds(mut self, seconds: u32) -> Self {
283 self.request.timeout = Some(seconds);
284 self
285 }
286
287 pub fn label(mut self, label: impl Into<String>) -> Self {
289 self.request.label = Some(label.into());
290 self
291 }
292
293 pub fn redaction(mut self, redaction: Redaction) -> Self {
295 self.request.redaction = Some(redaction);
296 self
297 }
298
299 pub fn build(self) -> PublishRequest {
301 self.request
302 }
303}
304
305#[derive(Debug, Clone)]
307pub struct BatchRequest {
308 pub(crate) publish: PublishRequest,
309 pub(crate) queue_name: Option<String>,
310}
311
312impl BatchRequest {
313 pub fn new(publish: PublishRequest) -> Self {
315 Self {
316 publish,
317 queue_name: None,
318 }
319 }
320
321 pub fn queue_name(mut self, queue_name: impl Into<String>) -> Self {
323 self.queue_name = Some(queue_name.into());
324 self
325 }
326}
327
328#[derive(Debug, Clone)]
330pub struct ScheduleRequest {
331 pub(crate) destination: Destination,
332 pub(crate) cron: String,
333 pub(crate) schedule_id: Option<String>,
334 pub(crate) body: Option<Bytes>,
335 pub(crate) headers: HeaderMap,
336 pub(crate) method: Method,
337 pub(crate) delay: Option<u32>,
338 pub(crate) retries: Option<u32>,
339 pub(crate) retry_delay: Option<String>,
340 pub(crate) callback: Option<String>,
341 pub(crate) failure_callback: Option<String>,
342 pub(crate) timeout: Option<u32>,
343 pub(crate) queue_name: Option<String>,
344 pub(crate) label: Option<String>,
345 pub(crate) redaction: Option<Redaction>,
346}
347
348impl ScheduleRequest {
349 pub fn builder(destination: Destination, cron: impl Into<String>) -> ScheduleRequestBuilder {
351 ScheduleRequestBuilder::new(destination, cron.into())
352 }
353}
354
355#[derive(Debug, Clone)]
357pub struct ScheduleRequestBuilder {
358 request: ScheduleRequest,
359}
360
361impl ScheduleRequestBuilder {
362 fn new(destination: Destination, cron: String) -> Self {
363 Self {
364 request: ScheduleRequest {
365 destination,
366 cron,
367 schedule_id: None,
368 body: None,
369 headers: HeaderMap::new(),
370 method: Method::POST,
371 delay: None,
372 retries: None,
373 retry_delay: None,
374 callback: None,
375 failure_callback: None,
376 timeout: None,
377 queue_name: None,
378 label: None,
379 redaction: None,
380 },
381 }
382 }
383
384 pub fn schedule_id(mut self, schedule_id: impl Into<String>) -> Self {
386 self.request.schedule_id = Some(schedule_id.into());
387 self
388 }
389
390 pub fn body(mut self, body: impl Into<Bytes>) -> Self {
392 self.request.body = Some(body.into());
393 self
394 }
395
396 pub fn json_body<T>(mut self, body: &T) -> Result<Self>
398 where
399 T: Serialize,
400 {
401 self.request.body = Some(Bytes::from(
402 serde_json::to_vec(body).map_err(Error::Serialize)?,
403 ));
404 self.request
405 .headers
406 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
407 Ok(self)
408 }
409
410 pub fn header(mut self, name: &str, value: &str) -> Result<Self> {
412 let header_name =
413 HeaderName::from_bytes(name.as_bytes()).map_err(|error| Error::InvalidRequest {
414 message: format!("invalid header name `{name}`: {error}"),
415 })?;
416 let header_value = HeaderValue::from_str(value).map_err(|error| Error::InvalidRequest {
417 message: format!("invalid header value for `{name}`: {error}"),
418 })?;
419 self.request.headers.insert(header_name, header_value);
420 Ok(self)
421 }
422
423 pub fn headers(mut self, headers: HeaderMap) -> Self {
425 self.request.headers = headers;
426 self
427 }
428
429 pub fn method(mut self, method: Method) -> Self {
431 self.request.method = method;
432 self
433 }
434
435 pub fn delay_seconds(mut self, seconds: u32) -> Self {
437 self.request.delay = Some(seconds);
438 self
439 }
440
441 pub fn retries(mut self, retries: u32) -> Self {
443 self.request.retries = Some(retries);
444 self
445 }
446
447 pub fn retry_delay(mut self, expression: impl Into<String>) -> Self {
449 self.request.retry_delay = Some(expression.into());
450 self
451 }
452
453 pub fn callback(mut self, url: impl Into<String>) -> Self {
455 self.request.callback = Some(url.into());
456 self
457 }
458
459 pub fn failure_callback(mut self, url: impl Into<String>) -> Self {
461 self.request.failure_callback = Some(url.into());
462 self
463 }
464
465 pub fn timeout_seconds(mut self, seconds: u32) -> Self {
467 self.request.timeout = Some(seconds);
468 self
469 }
470
471 pub fn queue_name(mut self, queue_name: impl Into<String>) -> Self {
473 self.request.queue_name = Some(queue_name.into());
474 self
475 }
476
477 pub fn label(mut self, label: impl Into<String>) -> Self {
479 self.request.label = Some(label.into());
480 self
481 }
482
483 pub fn redaction(mut self, redaction: Redaction) -> Self {
485 self.request.redaction = Some(redaction);
486 self
487 }
488
489 pub fn build(self) -> ScheduleRequest {
491 self.request
492 }
493}
494
495pub(crate) fn build_publish_headers(request: &PublishRequest) -> Result<HeaderMap> {
496 let mut headers = prefix_forward_headers(&request.headers)?;
497 headers.insert("Upstash-Method", header_value(request.method.as_str())?);
498
499 if let Some(delay) = request.delay {
500 headers.insert("Upstash-Delay", header_value(&format!("{delay}s"))?);
501 }
502
503 if let Some(not_before) = request.not_before {
504 headers.insert("Upstash-Not-Before", header_value(¬_before.to_string())?);
505 }
506
507 if let Some(deduplication_id) = &request.deduplication_id {
508 headers.insert("Upstash-Deduplication-Id", header_value(deduplication_id)?);
509 }
510
511 if request.content_based_deduplication {
512 headers.insert(
513 "Upstash-Content-Based-Deduplication",
514 HeaderValue::from_static("true"),
515 );
516 }
517
518 if let Some(retries) = request.retries {
519 headers.insert("Upstash-Retries", header_value(&retries.to_string())?);
520 }
521
522 if let Some(retry_delay) = &request.retry_delay {
523 headers.insert("Upstash-Retry-Delay", header_value(retry_delay)?);
524 }
525
526 if let Some(callback) = &request.callback {
527 headers.insert("Upstash-Callback", header_value(callback)?);
528 }
529
530 if let Some(failure_callback) = &request.failure_callback {
531 headers.insert("Upstash-Failure-Callback", header_value(failure_callback)?);
532 }
533
534 if let Some(timeout) = request.timeout {
535 headers.insert("Upstash-Timeout", header_value(&format!("{timeout}s"))?);
536 }
537
538 if let Some(label) = &request.label {
539 headers.insert("Upstash-Label", header_value(label)?);
540 }
541
542 if let Some(redaction) = &request.redaction {
543 if let Some(value) = redaction.header_value() {
544 headers.insert("Upstash-Redact-Fields", header_value(&value)?);
545 }
546 }
547
548 Ok(headers)
549}
550
551pub(crate) fn build_schedule_headers(request: &ScheduleRequest) -> Result<HeaderMap> {
552 let mut headers = prefix_forward_headers(&request.headers)?;
553
554 headers.insert("Upstash-Cron", header_value(&request.cron)?);
555 headers.insert("Upstash-Method", header_value(request.method.as_str())?);
556
557 if let Some(schedule_id) = &request.schedule_id {
558 headers.insert("Upstash-Schedule-Id", header_value(schedule_id)?);
559 }
560
561 if let Some(delay) = request.delay {
562 headers.insert("Upstash-Delay", header_value(&format!("{delay}s"))?);
563 }
564
565 if let Some(retries) = request.retries {
566 headers.insert("Upstash-Retries", header_value(&retries.to_string())?);
567 }
568
569 if let Some(retry_delay) = &request.retry_delay {
570 headers.insert("Upstash-Retry-Delay", header_value(retry_delay)?);
571 }
572
573 if let Some(callback) = &request.callback {
574 headers.insert("Upstash-Callback", header_value(callback)?);
575 }
576
577 if let Some(failure_callback) = &request.failure_callback {
578 headers.insert("Upstash-Failure-Callback", header_value(failure_callback)?);
579 }
580
581 if let Some(timeout) = request.timeout {
582 headers.insert("Upstash-Timeout", header_value(&format!("{timeout}s"))?);
583 }
584
585 if let Some(queue_name) = &request.queue_name {
586 headers.insert("Upstash-Queue-Name", header_value(queue_name)?);
587 }
588
589 if let Some(label) = &request.label {
590 headers.insert("Upstash-Label", header_value(label)?);
591 }
592
593 if let Some(redaction) = &request.redaction {
594 if let Some(value) = redaction.header_value() {
595 headers.insert("Upstash-Redact-Fields", header_value(&value)?);
596 }
597 }
598
599 Ok(headers)
600}
601
602fn prefix_forward_headers(headers: &HeaderMap) -> Result<HeaderMap> {
603 let mut prefixed = HeaderMap::new();
604
605 for (name, value) in headers {
606 if should_forward_header(name.as_str()) {
607 let forwarded_name =
608 HeaderName::from_bytes(format!("Upstash-Forward-{}", name.as_str()).as_bytes())
609 .map_err(|error| Error::InvalidRequest {
610 message: format!("invalid forwarded header name `{name}`: {error}"),
611 })?;
612 prefixed.insert(forwarded_name, value.clone());
613 } else {
614 prefixed.insert(name.clone(), value.clone());
615 }
616 }
617
618 Ok(prefixed)
619}
620
621fn should_forward_header(name: &str) -> bool {
622 let lower = name.to_ascii_lowercase();
623 !lower.starts_with("content-type") && !lower.starts_with("upstash-")
624}
625
626fn header_value(value: &str) -> Result<HeaderValue> {
627 HeaderValue::from_str(value).map_err(|error| Error::InvalidRequest {
628 message: format!("invalid header value `{value}`: {error}"),
629 })
630}