1use std::fmt;
5use std::sync::Arc;
6use std::time::Duration;
7
8use crate::base64;
9use crate::http::{self, Request, Response, Url};
10use crate::json::{self, Fields};
11use crate::transport::{TcpTransport, Timeouts, Transport, TransportError};
12
13pub const CONTRACT_VERSION: &str = "1";
15
16const LONG_POLL_SLACK: Duration = Duration::from_secs(10);
18
19#[derive(Debug, Clone)]
20pub struct Config {
21 pub base_url: String,
23 pub token: Option<String>,
24 pub connect_timeout: Duration,
25 pub request_timeout: Duration,
26 pub max_attempts: u32,
28 pub user_agent: String,
29 pub ca_pem: Option<Vec<u8>>,
31}
32
33impl Config {
34 pub fn new(base_url: impl Into<String>) -> Config {
35 Config {
36 base_url: base_url.into(),
37 token: None,
38 connect_timeout: Duration::from_secs(5),
39 request_timeout: Duration::from_secs(30),
40 max_attempts: 3,
41 user_agent: format!("taba-rust/{}", env!("CARGO_PKG_VERSION")),
42 ca_pem: None,
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Error {
51 pub status: u16,
52 pub code: String,
53 pub message: String,
54 pub request_id: Option<String>,
55 pub retry_after: Option<u64>,
57 pub details: Option<String>,
59 pub body: Vec<u8>,
61}
62
63impl Error {
64 pub fn from_message(code: &str, message: impl Into<String>) -> Error {
66 Error::client(code, message)
67 }
68
69 fn client(code: &str, message: impl Into<String>) -> Error {
70 Error {
71 status: 0,
72 code: code.to_owned(),
73 message: message.into(),
74 request_id: None,
75 retry_after: None,
76 details: None,
77 body: Vec::new(),
78 }
79 }
80
81 pub fn is_client_side(&self) -> bool {
83 self.status == 0
84 }
85}
86
87impl fmt::Display for Error {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 write!(f, "{}: {}", self.code, self.message)?;
90 if let Some(id) = &self.request_id {
91 write!(f, " (requestId {id})")?;
92 }
93 Ok(())
94 }
95}
96
97impl std::error::Error for Error {}
98
99#[derive(Debug, Clone)]
101pub struct Call {
102 pub method: &'static str,
103 pub path: String,
105 pub query: Vec<(String, String)>,
106 pub body: Vec<u8>,
107 pub headers: Vec<(String, String)>,
110 pub content_type: Option<String>,
111 pub request_id: Option<String>,
112 pub traceparent: Option<String>,
113 pub timeout: Option<Duration>,
115 pub retry_gateway: bool,
117 pub recover: Vec<(u16, &'static str)>,
120}
121
122impl Call {
123 pub fn new(method: &'static str, path: impl Into<String>) -> Call {
124 Call {
125 method,
126 path: path.into(),
127 query: Vec::new(),
128 body: Vec::new(),
129 headers: Vec::new(),
130 content_type: None,
131 request_id: None,
132 traceparent: None,
133 timeout: None,
134 retry_gateway: false,
135 recover: Vec::new(),
136 }
137 }
138
139 pub fn query(mut self, key: &str, value: impl ToString) -> Call {
140 self.query.push((key.to_owned(), value.to_string()));
141 self
142 }
143
144 pub fn json_body(mut self, body: String) -> Call {
145 self.body = body.into_bytes();
146 self
147 }
148}
149
150#[derive(Debug, Clone)]
152pub struct Reply {
153 pub status: u16,
154 pub headers: Vec<(String, String)>,
155 pub body: Vec<u8>,
156 pub request_id: Option<String>,
157 pub recovered: bool,
160}
161
162impl Reply {
163 pub fn text(&self) -> String {
164 String::from_utf8_lossy(&self.body).into_owned()
165 }
166}
167
168type Sleeper = Arc<dyn Fn(Duration) + Send + Sync>;
169
170pub struct Client {
171 config: Config,
172 url: Url,
173 transport: Box<dyn Transport>,
174 sleeper: Sleeper,
175}
176
177impl Client {
178 pub fn new(config: Config) -> Result<Client, Error> {
179 let transport = TcpTransport::new(config.ca_pem.clone());
180 Client::with_transport(config, Box::new(transport), Arc::new(std::thread::sleep))
181 }
182
183 pub fn with_transport(
185 config: Config,
186 transport: Box<dyn Transport>,
187 sleeper: Sleeper,
188 ) -> Result<Client, Error> {
189 let url =
190 Url::parse(&config.base_url).map_err(|m| Error::client("client.invalid-config", m))?;
191 Ok(Client {
192 config,
193 url,
194 transport,
195 sleeper,
196 })
197 }
198
199 pub fn config(&self) -> &Config {
200 &self.config
201 }
202
203 fn wire_request(&self, call: &Call) -> Request {
204 let mut headers = Vec::new();
205 if let Some(token) = &self.config.token {
206 headers.push(("Authorization".to_owned(), format!("Bearer {token}")));
207 }
208 if !call
209 .headers
210 .iter()
211 .any(|(k, _)| k.eq_ignore_ascii_case("accept"))
212 {
213 headers.push(("Accept".to_owned(), "application/json".to_owned()));
214 }
215 headers.extend(call.headers.iter().cloned());
216 headers.push(("User-Agent".to_owned(), self.config.user_agent.clone()));
217 if !call.body.is_empty() {
218 headers.push((
219 "Content-Type".to_owned(),
220 call.content_type
221 .clone()
222 .unwrap_or_else(|| "application/json".to_owned()),
223 ));
224 }
225 if let Some(id) = &call.request_id {
226 headers.push(("X-Request-Id".to_owned(), id.clone()));
227 }
228 if let Some(tp) = &call.traceparent {
229 headers.push(("traceparent".to_owned(), tp.clone()));
230 }
231 Request {
232 method: call.method.to_owned(),
233 target: http::build_target(&self.url.base_path, &call.path, &call.query),
234 headers,
235 body: call.body.clone(),
236 }
237 }
238
239 pub fn call(&self, call: &Call) -> Result<Reply, Error> {
244 let request = self.wire_request(call);
245 let timeouts = Timeouts {
246 connect: self.config.connect_timeout,
247 total: call.timeout.unwrap_or(self.config.request_timeout),
248 };
249 let max = self.config.max_attempts.max(1);
250 let mut attempt = 1;
251 let mut after_503 = false;
252 loop {
253 let backoff = Duration::from_secs(1 << (attempt - 1).min(6));
254 match self.transport.send(&self.url, &request, &timeouts) {
255 Ok(response) => {
256 if response.status < 400 {
257 return Ok(reply(response, false));
258 }
259 if after_503
260 && call.recover.iter().any(|(status, code)| {
261 *status == response.status
262 && error_code(&response).as_deref() == Some(*code)
263 })
264 {
265 return Ok(reply(response, true));
266 }
267 let error = error_from(&response);
268 let retry = match response.status {
269 429 | 503 => true,
270 502 | 504 => call.retry_gateway,
271 _ => false,
272 };
273 if !retry || attempt >= max {
274 return Err(error);
275 }
276 after_503 |= response.status == 503;
277 (self.sleeper)(
278 error
279 .retry_after
280 .map(Duration::from_secs)
281 .unwrap_or(backoff),
282 );
283 }
284 Err(TransportError::Connect(message)) => {
285 if attempt >= max {
286 return Err(Error::client("client.network", message));
287 }
288 after_503 = true;
289 (self.sleeper)(backoff);
290 }
291 Err(TransportError::Timeout { sent: false }) => {
292 if attempt >= max {
293 return Err(Error::client(
294 "client.timeout",
295 "timed out opening the connection",
296 ));
297 }
298 after_503 = true;
299 (self.sleeper)(backoff);
300 }
301 Err(TransportError::Timeout { sent: true }) => {
302 return Err(Error::client(
303 "client.timeout",
304 "timed out waiting for the response",
305 ));
306 }
307 Err(TransportError::Io(message)) => {
308 return Err(Error::client("client.network", message))
309 }
310 Err(TransportError::Protocol(message)) => {
311 return Err(Error::client(
312 "client.network",
313 format!("invalid response: {message}"),
314 ))
315 }
316 }
317 attempt += 1;
318 }
319 }
320}
321
322fn reply(response: Response, recovered: bool) -> Reply {
323 Reply {
324 request_id: response.header("x-request-id").map(str::to_owned),
325 status: response.status,
326 headers: response.headers,
327 body: response.body,
328 recovered,
329 }
330}
331
332fn error_code(response: &Response) -> Option<String> {
333 let text = std::str::from_utf8(&response.body).ok()?;
334 Fields::parse(text).ok()?.string("code")
335}
336
337fn error_from(response: &Response) -> Error {
339 let text = String::from_utf8_lossy(&response.body);
340 let fields = Fields::parse(&text).ok();
341 let header_request_id = response.header("x-request-id").map(str::to_owned);
342 let (code, message, request_id, details) = match &fields {
343 Some(f) if f.string("code").is_some() => (
344 f.string("code").unwrap_or_default(),
345 f.string("message").unwrap_or_default(),
346 f.string("requestId").or(header_request_id),
347 f.raw("details").map(str::to_owned),
348 ),
349 _ => (
350 format!("http.{}", response.status),
351 if text.trim().is_empty() {
352 format!("response {} with no body", response.status)
353 } else {
354 text.trim().chars().take(300).collect()
355 },
356 header_request_id,
357 None,
358 ),
359 };
360 Error {
361 status: response.status,
362 code,
363 message,
364 request_id,
365 retry_after: response
366 .header("retry-after")
367 .and_then(|v| v.trim().parse().ok()),
368 details,
369 body: response.body.clone(),
370 }
371}
372
373fn bad_response(what: &str, e: json::JsonError) -> Error {
378 Error::client("client.invalid-response", format!("{what}: {e}"))
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct Topic {
383 pub id: String,
384 pub name: String,
385 pub topic_type: String,
387 pub created_at: String,
388 pub fifo: Option<bool>,
389 pub volatile: Option<bool>,
390 pub raw: String,
392}
393
394impl Topic {
395 pub fn parse(raw: &str) -> Result<Topic, Error> {
396 let f = Fields::parse(raw).map_err(|e| bad_response("topic", e))?;
397 Ok(Topic {
398 id: f.string("id").unwrap_or_default(),
399 name: f.string("name").unwrap_or_default(),
400 topic_type: f.string("type").unwrap_or_default(),
401 created_at: f.string("createdAt").unwrap_or_default(),
402 fifo: f.boolean("fifo"),
403 volatile: f.boolean("volatile"),
404 raw: raw.to_owned(),
405 })
406 }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
411pub struct Page<T> {
412 pub items: Vec<T>,
413 pub next_cursor: Option<String>,
414 pub raw: String,
415}
416
417fn parse_page<T>(raw: &str, item: impl Fn(&str) -> Result<T, Error>) -> Result<Page<T>, Error> {
418 let f = Fields::parse(raw).map_err(|e| bad_response("page", e))?;
419 let items_raw = f.raw("items").unwrap_or("[]");
420 let items = json::array_items(items_raw)
421 .map_err(|e| bad_response("page items", e))?
422 .into_iter()
423 .map(item)
424 .collect::<Result<Vec<_>, _>>()?;
425 Ok(Page {
426 items,
427 next_cursor: f.string("nextCursor"),
428 raw: raw.to_owned(),
429 })
430}
431
432#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct Message {
435 pub id: String,
436 pub body: Vec<u8>,
437 pub content_type: String,
438 pub receipt_token: String,
439 pub delivery_count: u64,
440 pub published_at: String,
441 pub invisible_until: String,
442 pub group_id: Option<String>,
443 pub deduplication_id: Option<String>,
444 pub dead_lettered_at: Option<String>,
445 pub dead_letter_reason: Option<String>,
446 pub raw: String,
448}
449
450fn is_json_type(content_type: &str) -> bool {
451 let media = content_type
452 .split(';')
453 .next()
454 .unwrap_or_default()
455 .trim()
456 .to_ascii_lowercase();
457 media == "application/json" || media.ends_with("+json")
458}
459
460impl Message {
461 pub fn parse(raw: &str) -> Result<Message, Error> {
462 let f = Fields::parse(raw).map_err(|e| bad_response("message", e))?;
463 let content_type = f
464 .string("contentType")
465 .unwrap_or_else(|| "application/json".to_owned());
466 let body = match f.raw("body") {
467 None => Vec::new(),
468 Some(body) if f.string("bodyEncoding").as_deref() == Some("base64") => {
469 let text =
470 json::decode_string(body).map_err(|e| bad_response("message body", e))?;
471 base64::decode(&text).ok_or_else(|| {
472 Error::client("client.invalid-response", "invalid base64 body")
473 })?
474 }
475 Some(body) if is_json_type(&content_type) => body.as_bytes().to_vec(),
476 Some(body) => json::decode_string(body)
477 .map_err(|e| bad_response("message body", e))?
478 .into_bytes(),
479 };
480 Ok(Message {
481 id: f.string("id").unwrap_or_default(),
482 body,
483 content_type,
484 receipt_token: f.string("receiptToken").unwrap_or_default(),
485 delivery_count: f.unsigned("deliveryCount").unwrap_or(0),
486 published_at: f.string("publishedAt").unwrap_or_default(),
487 invisible_until: f.string("invisibleUntil").unwrap_or_default(),
488 group_id: f.string("groupId"),
489 deduplication_id: f.string("deduplicationId"),
490 dead_lettered_at: f.string("deadLetteredAt"),
491 dead_letter_reason: f.string("deadLetterReason"),
492 raw: raw.to_owned(),
493 })
494 }
495}
496
497#[derive(Debug, Clone, PartialEq, Eq)]
498pub struct PublishResponse {
499 pub id: Option<String>,
501 pub duplicate: Option<bool>,
503 pub matched: Option<u64>,
505 pub raw: String,
506}
507
508#[derive(Debug, Clone, PartialEq, Eq)]
509pub struct VisibilityResponse {
510 pub id: String,
511 pub invisible_until: String,
512 pub raw: String,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq)]
518pub struct BatchMessage {
519 pub body: Vec<u8>,
520 pub content_type: Option<String>,
522 pub group_id: Option<String>,
524 pub deduplication_id: Option<String>,
526}
527
528impl BatchMessage {
529 pub fn new(body: impl Into<Vec<u8>>) -> Self {
530 Self {
531 body: body.into(),
532 content_type: None,
533 group_id: None,
534 deduplication_id: None,
535 }
536 }
537
538 pub fn text(text: &str) -> Self {
540 Self::new(text.as_bytes()).content_type("text/plain; charset=utf-8")
541 }
542
543 pub fn content_type(mut self, content_type: &str) -> Self {
544 self.content_type = Some(content_type.to_owned());
545 self
546 }
547
548 pub fn group_id(mut self, group_id: &str) -> Self {
549 self.group_id = Some(group_id.to_owned());
550 self
551 }
552
553 pub fn deduplication_id(mut self, deduplication_id: &str) -> Self {
554 self.deduplication_id = Some(deduplication_id.to_owned());
555 self
556 }
557
558 fn to_json(&self) -> String {
559 let mut out = format!(
560 "{{\"body\":\"{}\",\"bodyEncoding\":\"base64\"",
561 crate::base64::encode(&self.body)
562 );
563 for (key, value) in [
564 ("contentType", &self.content_type),
565 ("groupId", &self.group_id),
566 ("deduplicationId", &self.deduplication_id),
567 ] {
568 if let Some(value) = value {
569 out.push_str(&format!(",\"{key}\":{}", json::quote(value)));
570 }
571 }
572 out.push('}');
573 out
574 }
575}
576
577#[derive(Debug, Clone, PartialEq, Eq)]
580pub struct BatchResult {
581 pub status: u16,
583 pub id: Option<String>,
584 pub duplicate: Option<bool>,
586 pub matched: Option<u64>,
588 pub invisible_until: Option<String>,
590 pub error_code: Option<String>,
591 pub error_message: Option<String>,
592 pub raw: String,
593}
594
595impl BatchResult {
596 pub fn is_ok(&self) -> bool {
598 self.error_code.is_none() && self.status < 300
599 }
600
601 fn parse_all(raw: &str) -> Result<Vec<Self>, Error> {
602 let f = Fields::parse(raw).map_err(|e| bad_response("lote", e))?;
603 json::array_items(f.raw("results").unwrap_or("[]"))
604 .map_err(|e| bad_response("results", e))?
605 .into_iter()
606 .map(|item| {
607 let r = Fields::parse(item).map_err(|e| bad_response("results", e))?;
608 let error = match r.raw("error") {
609 Some(e) if e.starts_with('{') => {
610 Some(Fields::parse(e).map_err(|e| bad_response("error", e))?)
611 }
612 _ => None,
613 };
614 Ok(Self {
615 status: r
616 .unsigned("status")
617 .and_then(|s| u16::try_from(s).ok())
618 .unwrap_or(0),
619 id: r.string("id"),
620 duplicate: r.boolean("duplicate"),
621 matched: r.unsigned("matched"),
622 invisible_until: r.string("invisibleUntil"),
623 error_code: error.as_ref().map(|e| e.string("code").unwrap_or_default()),
624 error_message: error.as_ref().and_then(|e| e.string("message")),
625 raw: item.to_owned(),
626 })
627 })
628 .collect()
629 }
630}
631
632fn receipts_json(messages: &[Message], extra: &str) -> String {
634 let items: Vec<String> = messages
635 .iter()
636 .map(|m| {
637 format!(
638 "{{\"id\":{},\"receiptToken\":{}{extra}}}",
639 json::quote(&m.id),
640 json::quote(&m.receipt_token)
641 )
642 })
643 .collect();
644 format!("{{\"messages\":[{}]}}", items.join(","))
645}
646
647fn json_map(pairs: &[(String, String)]) -> String {
652 let items: Vec<String> = pairs
653 .iter()
654 .map(|(k, v)| format!("{}:{}", json::quote(k), json::quote(v)))
655 .collect();
656 format!("{{{}}}", items.join(","))
657}
658
659fn json_list(items: &[String]) -> String {
660 let items: Vec<String> = items.iter().map(|i| json::quote(i)).collect();
661 format!("[{}]", items.join(","))
662}
663
664#[derive(Debug, Clone, Default)]
667pub struct TopicRequest {
668 pub name: String,
669 pub topic_type: String,
671 pub tags: Vec<(String, String)>,
672 pub prefixes: Vec<String>,
673 pub volatile: Option<bool>,
674 pub idle_timeout: Option<u64>,
675 pub fifo: Option<bool>,
676 pub max_message_bytes: Option<u64>,
677 pub ttl: Option<u64>,
678 pub max_deliveries: Option<u64>,
679}
680
681impl TopicRequest {
682 pub fn to_json(&self) -> String {
683 let mut fields = vec![
684 format!("\"name\":{}", json::quote(&self.name)),
685 format!("\"type\":{}", json::quote(&self.topic_type)),
686 ];
687 if !self.tags.is_empty() {
688 fields.push(format!("\"tags\":{}", json_map(&self.tags)));
689 }
690 if !self.prefixes.is_empty() {
691 fields.push(format!("\"prefixes\":{}", json_list(&self.prefixes)));
692 }
693 if let Some(v) = self.volatile {
694 fields.push(format!("\"volatile\":{v}"));
695 }
696 if let Some(v) = self.idle_timeout {
697 fields.push(format!("\"idleTimeout\":{v}"));
698 }
699 if let Some(v) = self.fifo {
700 fields.push(format!("\"fifo\":{v}"));
701 }
702 if let Some(v) = self.max_message_bytes {
703 fields.push(format!("\"maxMessageBytes\":{v}"));
704 }
705 if let Some(v) = self.ttl {
706 fields.push(format!("\"ttl\":{v}"));
707 }
708 if let Some(v) = self.max_deliveries {
709 fields.push(format!("\"maxDeliveries\":{v}"));
710 }
711 format!("{{{}}}", fields.join(","))
712 }
713}
714
715#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
717pub enum Nullable<T> {
718 #[default]
720 Keep,
721 Default,
723 Set(T),
724}
725
726impl<T: fmt::Display> Nullable<T> {
727 fn push(&self, name: &str, fields: &mut Vec<String>) {
728 match self {
729 Nullable::Keep => {}
730 Nullable::Default => fields.push(format!("\"{name}\":null")),
731 Nullable::Set(v) => fields.push(format!("\"{name}\":{v}")),
732 }
733 }
734}
735
736#[derive(Debug, Clone, Default)]
738pub struct TopicUpdate {
739 pub tags: Option<Vec<(String, String)>>,
741 pub prefixes: Option<Vec<String>>,
742 pub idle_timeout: Option<u64>,
743 pub max_message_bytes: Nullable<u64>,
744 pub ttl: Nullable<u64>,
745 pub max_deliveries: Nullable<u64>,
746}
747
748impl TopicUpdate {
749 pub fn to_json(&self) -> String {
750 let mut fields = Vec::new();
751 if let Some(tags) = &self.tags {
752 fields.push(format!("\"tags\":{}", json_map(tags)));
753 }
754 if let Some(prefixes) = &self.prefixes {
755 fields.push(format!("\"prefixes\":{}", json_list(prefixes)));
756 }
757 if let Some(v) = self.idle_timeout {
758 fields.push(format!("\"idleTimeout\":{v}"));
759 }
760 self.max_message_bytes.push("maxMessageBytes", &mut fields);
761 self.ttl.push("ttl", &mut fields);
762 self.max_deliveries.push("maxDeliveries", &mut fields);
763 format!("{{{}}}", fields.join(","))
764 }
765}
766
767#[derive(Debug, Clone, Default)]
768pub struct PublishOptions {
769 pub content_type: Option<String>,
771 pub group_id: Option<String>,
772 pub deduplication_id: Option<String>,
773 pub request_id: Option<String>,
774 pub fifo: bool,
776}
777
778#[derive(Debug, Clone, Default)]
779pub struct ConsumeOptions {
780 pub max_messages: Option<u32>,
781 pub visibility_timeout: Option<u32>,
782 pub wait_time: Option<u32>,
783 pub auto_create: bool,
784 pub volatile: Option<bool>,
786 pub idle_timeout: Option<u32>,
787 pub fifo: Option<bool>,
788 pub tags: Vec<(String, String)>,
789 pub request_id: Option<String>,
790}
791
792fn segment(s: &str) -> String {
793 http::percent_encode(s)
794}
795
796impl Client {
801 pub fn create_topic(&self, request: &TopicRequest) -> Result<Topic, Error> {
803 let mut call = Call::new("POST", "/topics").json_body(request.to_json());
804 call.recover = vec![(409, "topic.already-exists")];
805 let reply = self.call(&call)?;
806 if reply.recovered {
807 return self.get_topic(&request.name);
808 }
809 Topic::parse(&reply.text())
810 }
811
812 pub fn get_topic(&self, name: &str) -> Result<Topic, Error> {
814 let reply = self.call(&Call::new("GET", format!("/topics/{}", segment(name))))?;
815 Topic::parse(&reply.text())
816 }
817
818 pub fn list_topics(
820 &self,
821 limit: Option<u32>,
822 cursor: Option<&str>,
823 name: Option<&str>,
824 ) -> Result<Page<Topic>, Error> {
825 let mut call = Call::new("GET", "/topics");
826 if let Some(limit) = limit {
827 call = call.query("limit", limit);
828 }
829 if let Some(cursor) = cursor {
830 call = call.query("cursor", cursor);
831 }
832 if let Some(name) = name {
833 call = call.query("name", name);
834 }
835 parse_page(&self.call(&call)?.text(), Topic::parse)
836 }
837
838 pub fn list_all_topics(&self, name: Option<&str>) -> Result<Vec<Topic>, Error> {
840 let mut all = Vec::new();
841 let mut cursor: Option<String> = None;
842 loop {
843 let page = self.list_topics(None, cursor.as_deref(), name)?;
844 all.extend(page.items);
845 match page.next_cursor {
846 Some(next) => cursor = Some(next),
847 None => return Ok(all),
848 }
849 }
850 }
851
852 pub fn update_topic(&self, name: &str, update: &TopicUpdate) -> Result<Topic, Error> {
854 let call =
855 Call::new("PATCH", format!("/topics/{}", segment(name))).json_body(update.to_json());
856 Topic::parse(&self.call(&call)?.text())
857 }
858
859 pub fn delete_topic(&self, name: &str) -> Result<(), Error> {
861 let mut call = Call::new("DELETE", format!("/topics/{}", segment(name)));
862 call.recover = vec![(404, "topic.not-found")];
863 self.call(&call).map(|_| ())
864 }
865
866 pub fn publish(
868 &self,
869 topic: &str,
870 body: &[u8],
871 options: &PublishOptions,
872 ) -> Result<PublishResponse, Error> {
873 let mut call = Call::new("POST", format!("/topics/{}/messages", segment(topic)));
874 call.body = body.to_vec();
875 call.content_type = Some(
876 options
877 .content_type
878 .clone()
879 .unwrap_or_else(|| "application/json".to_owned()),
880 );
881 if let Some(group) = &options.group_id {
882 call = call.query("groupId", group);
883 }
884 if let Some(dedup) = &options.deduplication_id {
885 call = call.query("deduplicationId", dedup);
886 }
887 call.request_id = options.request_id.clone();
888 call.retry_gateway = options.fifo;
889 let raw = self.call(&call)?.text();
891 let f = Fields::parse(&raw).map_err(|e| bad_response("publication", e))?;
892 Ok(PublishResponse {
893 id: f.string("id"),
894 duplicate: f.boolean("duplicate"),
895 matched: f.unsigned("matched"),
896 raw,
897 })
898 }
899
900 pub fn publish_json(&self, topic: &str, json_text: &str) -> Result<PublishResponse, Error> {
901 self.publish(topic, json_text.as_bytes(), &PublishOptions::default())
902 }
903
904 pub fn publish_text(&self, topic: &str, text: &str) -> Result<PublishResponse, Error> {
905 let options = PublishOptions {
906 content_type: Some("text/plain; charset=utf-8".to_owned()),
907 ..PublishOptions::default()
908 };
909 self.publish(topic, text.as_bytes(), &options)
910 }
911
912 pub fn publish_batch(
917 &self,
918 topic: &str,
919 messages: &[BatchMessage],
920 ) -> Result<Vec<BatchResult>, Error> {
921 let items: Vec<String> = messages.iter().map(BatchMessage::to_json).collect();
922 let call = Call::new("POST", format!("/topics/{}/messages/batch", segment(topic)))
923 .json_body(format!("{{\"messages\":[{}]}}", items.join(",")));
924 BatchResult::parse_all(&self.call(&call)?.text())
925 }
926
927 pub fn ack_batch(&self, topic: &str, messages: &[Message]) -> Result<Vec<BatchResult>, Error> {
930 let call = Call::new("POST", format!("/topics/{}/messages/ack", segment(topic)))
931 .json_body(receipts_json(messages, ""));
932 BatchResult::parse_all(&self.call(&call)?.text())
933 }
934
935 pub fn change_visibility_batch(
938 &self,
939 topic: &str,
940 messages: &[Message],
941 seconds: u32,
942 ) -> Result<Vec<BatchResult>, Error> {
943 let call = Call::new(
944 "POST",
945 format!("/topics/{}/messages/visibility", segment(topic)),
946 )
947 .json_body(receipts_json(
948 messages,
949 &format!(",\"visibilityTimeout\":{seconds}"),
950 ));
951 BatchResult::parse_all(&self.call(&call)?.text())
952 }
953
954 pub fn consume(&self, topic: &str, options: &ConsumeOptions) -> Result<Vec<Message>, Error> {
957 let call = self.consume_call(topic, options);
958 let raw = self.call(&call)?.text();
959 let f = Fields::parse(&raw).map_err(|e| bad_response("consumo", e))?;
960 json::array_items(f.raw("messages").unwrap_or("[]"))
961 .map_err(|e| bad_response("messages", e))?
962 .into_iter()
963 .map(Message::parse)
964 .collect()
965 }
966
967 pub fn consume_call(&self, topic: &str, options: &ConsumeOptions) -> Call {
968 let mut call = Call::new("GET", format!("/topics/{}/messages", segment(topic)));
969 if let Some(v) = options.max_messages {
970 call = call.query("maxMessages", v);
971 }
972 if let Some(v) = options.visibility_timeout {
973 call = call.query("visibilityTimeout", v);
974 }
975 if let Some(v) = options.wait_time {
976 call = call.query("waitTime", v);
977 }
978 if options.auto_create {
979 call = call.query("autoCreate", true);
980 if let Some(v) = options.volatile {
981 call = call.query("volatile", v);
982 }
983 if let Some(v) = options.idle_timeout {
984 call = call.query("idleTimeout", v);
985 }
986 if let Some(v) = options.fifo {
987 call = call.query("fifo", v);
988 }
989 for (k, v) in &options.tags {
990 call = call.query(&format!("tag.{k}"), v);
991 }
992 }
993 call.request_id = options.request_id.clone();
994 call.timeout =
995 Some(Duration::from_secs(u64::from(options.wait_time.unwrap_or(0))) + LONG_POLL_SLACK);
996 call
997 }
998
999 pub fn ack(&self, topic: &str, message_id: &str, receipt_token: &str) -> Result<(), Error> {
1001 let call = Call::new(
1002 "DELETE",
1003 format!(
1004 "/topics/{}/messages/{}",
1005 segment(topic),
1006 segment(message_id)
1007 ),
1008 )
1009 .query("receiptToken", receipt_token);
1010 self.call(&call).map(|_| ())
1011 }
1012
1013 pub fn change_visibility(
1015 &self,
1016 topic: &str,
1017 message_id: &str,
1018 receipt_token: &str,
1019 seconds: u32,
1020 ) -> Result<VisibilityResponse, Error> {
1021 let call = Call::new(
1022 "PUT",
1023 format!(
1024 "/topics/{}/messages/{}/visibility",
1025 segment(topic),
1026 segment(message_id)
1027 ),
1028 )
1029 .query("receiptToken", receipt_token)
1030 .json_body(format!("{{\"visibilityTimeout\":{seconds}}}"));
1031 let raw = self.call(&call)?.text();
1032 let f = Fields::parse(&raw).map_err(|e| bad_response("invisibilidade", e))?;
1033 Ok(VisibilityResponse {
1034 id: f.string("id").unwrap_or_default(),
1035 invisible_until: f.string("invisibleUntil").unwrap_or_default(),
1036 raw,
1037 })
1038 }
1039
1040 pub fn subscribe<H, S>(
1045 &self,
1046 topic: &str,
1047 options: &ConsumeOptions,
1048 mut stop: S,
1049 mut handler: H,
1050 ) -> Result<(), Error>
1051 where
1052 H: FnMut(&Message) -> Result<(), Box<dyn std::error::Error>>,
1053 S: FnMut() -> bool,
1054 {
1055 let mut options = options.clone();
1056 options.wait_time.get_or_insert(20);
1057 options.max_messages.get_or_insert(1);
1058 while !stop() {
1059 for message in self.consume(topic, &options)? {
1060 if handler(&message).is_ok() {
1061 match self.ack(topic, &message.id, &message.receipt_token) {
1062 Err(e) if e.status == 409 || e.status == 404 => {}
1065 other => other?,
1066 }
1067 }
1068 if stop() {
1069 return Ok(());
1070 }
1071 }
1072 }
1073 Ok(())
1074 }
1075}
1076
1077mod handlers;
1078pub use handlers::{
1079 ConsumeError, Delivery, HandlerError, DEFAULT_MAX_MESSAGES, DEFAULT_VISIBILITY_TIMEOUT,
1080 DEFAULT_WAIT_TIME,
1081};
1082
1083#[cfg(test)]
1084mod tests;