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![format!("\"name\":{}", json::quote(&self.name))];
684 self.push_fields(&mut fields);
685 format!("{{{}}}", fields.join(","))
686 }
687
688 pub fn to_put_json(&self) -> String {
690 let mut fields = Vec::new();
691 self.push_fields(&mut fields);
692 format!("{{{}}}", fields.join(","))
693 }
694
695 fn push_fields(&self, fields: &mut Vec<String>) {
696 fields.push(format!("\"type\":{}", json::quote(&self.topic_type)));
697 if !self.tags.is_empty() {
698 fields.push(format!("\"tags\":{}", json_map(&self.tags)));
699 }
700 if !self.prefixes.is_empty() {
701 fields.push(format!("\"prefixes\":{}", json_list(&self.prefixes)));
702 }
703 if let Some(v) = self.volatile {
704 fields.push(format!("\"volatile\":{v}"));
705 }
706 if let Some(v) = self.idle_timeout {
707 fields.push(format!("\"idleTimeout\":{v}"));
708 }
709 if let Some(v) = self.fifo {
710 fields.push(format!("\"fifo\":{v}"));
711 }
712 if let Some(v) = self.max_message_bytes {
713 fields.push(format!("\"maxMessageBytes\":{v}"));
714 }
715 if let Some(v) = self.ttl {
716 fields.push(format!("\"ttl\":{v}"));
717 }
718 if let Some(v) = self.max_deliveries {
719 fields.push(format!("\"maxDeliveries\":{v}"));
720 }
721 }
722}
723
724#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
727pub enum Recreate {
728 #[default]
730 Never,
731 IfEmpty,
734 Always,
736}
737
738impl Recreate {
739 fn query_value(self) -> Option<&'static str> {
740 match self {
741 Recreate::Never => None,
742 Recreate::IfEmpty => Some("if-empty"),
743 Recreate::Always => Some("always"),
744 }
745 }
746}
747
748#[derive(Debug, Clone, Copy, PartialEq, Eq)]
750pub enum TopicOutcome {
751 Created,
753 Updated,
755 Recreated,
757}
758
759#[derive(Debug, Clone, PartialEq, Eq)]
761pub struct TopicPut {
762 pub topic: Topic,
763 pub outcome: TopicOutcome,
764}
765
766#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
768pub enum Nullable<T> {
769 #[default]
771 Keep,
772 Default,
774 Set(T),
775}
776
777impl<T: fmt::Display> Nullable<T> {
778 fn push(&self, name: &str, fields: &mut Vec<String>) {
779 match self {
780 Nullable::Keep => {}
781 Nullable::Default => fields.push(format!("\"{name}\":null")),
782 Nullable::Set(v) => fields.push(format!("\"{name}\":{v}")),
783 }
784 }
785}
786
787#[derive(Debug, Clone, Default)]
789pub struct TopicUpdate {
790 pub tags: Option<Vec<(String, String)>>,
792 pub prefixes: Option<Vec<String>>,
793 pub idle_timeout: Option<u64>,
794 pub max_message_bytes: Nullable<u64>,
795 pub ttl: Nullable<u64>,
796 pub max_deliveries: Nullable<u64>,
797}
798
799impl TopicUpdate {
800 pub fn to_json(&self) -> String {
801 let mut fields = Vec::new();
802 if let Some(tags) = &self.tags {
803 fields.push(format!("\"tags\":{}", json_map(tags)));
804 }
805 if let Some(prefixes) = &self.prefixes {
806 fields.push(format!("\"prefixes\":{}", json_list(prefixes)));
807 }
808 if let Some(v) = self.idle_timeout {
809 fields.push(format!("\"idleTimeout\":{v}"));
810 }
811 self.max_message_bytes.push("maxMessageBytes", &mut fields);
812 self.ttl.push("ttl", &mut fields);
813 self.max_deliveries.push("maxDeliveries", &mut fields);
814 format!("{{{}}}", fields.join(","))
815 }
816}
817
818#[derive(Debug, Clone, Default)]
819pub struct PublishOptions {
820 pub content_type: Option<String>,
822 pub group_id: Option<String>,
823 pub deduplication_id: Option<String>,
824 pub request_id: Option<String>,
825 pub fifo: bool,
827}
828
829#[derive(Debug, Clone, Default)]
830pub struct ConsumeOptions {
831 pub max_messages: Option<u32>,
832 pub visibility_timeout: Option<u32>,
833 pub wait_time: Option<u32>,
834 pub auto_create: bool,
835 pub volatile: Option<bool>,
837 pub idle_timeout: Option<u32>,
838 pub fifo: Option<bool>,
839 pub tags: Vec<(String, String)>,
840 pub request_id: Option<String>,
841}
842
843fn segment(s: &str) -> String {
844 http::percent_encode(s)
845}
846
847impl Client {
852 pub fn create_topic(&self, request: &TopicRequest) -> Result<Topic, Error> {
854 let mut call = Call::new("POST", "/topics").json_body(request.to_json());
855 call.recover = vec![(409, "topic.already-exists")];
856 let reply = self.call(&call)?;
857 if reply.recovered {
858 return self.get_topic(&request.name);
859 }
860 Topic::parse(&reply.text())
861 }
862
863 pub fn get_topic(&self, name: &str) -> Result<Topic, Error> {
865 let reply = self.call(&Call::new("GET", format!("/topics/{}", segment(name))))?;
866 Topic::parse(&reply.text())
867 }
868
869 pub fn list_topics(
871 &self,
872 limit: Option<u32>,
873 cursor: Option<&str>,
874 name: Option<&str>,
875 ) -> Result<Page<Topic>, Error> {
876 let mut call = Call::new("GET", "/topics");
877 if let Some(limit) = limit {
878 call = call.query("limit", limit);
879 }
880 if let Some(cursor) = cursor {
881 call = call.query("cursor", cursor);
882 }
883 if let Some(name) = name {
884 call = call.query("name", name);
885 }
886 parse_page(&self.call(&call)?.text(), Topic::parse)
887 }
888
889 pub fn list_all_topics(&self, name: Option<&str>) -> Result<Vec<Topic>, Error> {
891 let mut all = Vec::new();
892 let mut cursor: Option<String> = None;
893 loop {
894 let page = self.list_topics(None, cursor.as_deref(), name)?;
895 all.extend(page.items);
896 match page.next_cursor {
897 Some(next) => cursor = Some(next),
898 None => return Ok(all),
899 }
900 }
901 }
902
903 pub fn update_topic(&self, name: &str, update: &TopicUpdate) -> Result<Topic, Error> {
905 let call =
906 Call::new("PATCH", format!("/topics/{}", segment(name))).json_body(update.to_json());
907 Topic::parse(&self.call(&call)?.text())
908 }
909
910 pub fn create_or_update_topic(
915 &self,
916 request: &TopicRequest,
917 recreate: Recreate,
918 ) -> Result<TopicPut, Error> {
919 let mut call = Call::new("PUT", format!("/topics/{}", segment(&request.name)))
920 .json_body(request.to_put_json());
921 if let Some(value) = recreate.query_value() {
922 call = call.query("recreate", value);
923 }
924 let reply = self.call(&call)?;
925 let recreated = reply.headers.iter().any(|(k, v)| {
926 k.eq_ignore_ascii_case("taba-topic-recreated") && v.trim().eq_ignore_ascii_case("true")
927 });
928 let outcome = match reply.status {
929 201 if recreated => TopicOutcome::Recreated,
930 201 => TopicOutcome::Created,
931 _ => TopicOutcome::Updated,
932 };
933 Ok(TopicPut {
934 topic: Topic::parse(&reply.text())?,
935 outcome,
936 })
937 }
938
939 pub fn delete_topic(&self, name: &str) -> Result<(), Error> {
941 let mut call = Call::new("DELETE", format!("/topics/{}", segment(name)));
942 call.recover = vec![(404, "topic.not-found")];
943 self.call(&call).map(|_| ())
944 }
945
946 pub fn delete_topic_if_empty(&self, name: &str) -> Result<(), Error> {
949 let mut call =
950 Call::new("DELETE", format!("/topics/{}", segment(name))).query("ifEmpty", "true");
951 call.recover = vec![(404, "topic.not-found")];
952 self.call(&call).map(|_| ())
953 }
954
955 pub fn publish(
957 &self,
958 topic: &str,
959 body: &[u8],
960 options: &PublishOptions,
961 ) -> Result<PublishResponse, Error> {
962 let mut call = Call::new("POST", format!("/topics/{}/messages", segment(topic)));
963 call.body = body.to_vec();
964 call.content_type = Some(
965 options
966 .content_type
967 .clone()
968 .unwrap_or_else(|| "application/json".to_owned()),
969 );
970 if let Some(group) = &options.group_id {
971 call = call.query("groupId", group);
972 }
973 if let Some(dedup) = &options.deduplication_id {
974 call = call.query("deduplicationId", dedup);
975 }
976 call.request_id = options.request_id.clone();
977 call.retry_gateway = options.fifo;
978 let raw = self.call(&call)?.text();
980 let f = Fields::parse(&raw).map_err(|e| bad_response("publication", e))?;
981 Ok(PublishResponse {
982 id: f.string("id"),
983 duplicate: f.boolean("duplicate"),
984 matched: f.unsigned("matched"),
985 raw,
986 })
987 }
988
989 pub fn publish_json(&self, topic: &str, json_text: &str) -> Result<PublishResponse, Error> {
990 self.publish(topic, json_text.as_bytes(), &PublishOptions::default())
991 }
992
993 pub fn publish_text(&self, topic: &str, text: &str) -> Result<PublishResponse, Error> {
994 let options = PublishOptions {
995 content_type: Some("text/plain; charset=utf-8".to_owned()),
996 ..PublishOptions::default()
997 };
998 self.publish(topic, text.as_bytes(), &options)
999 }
1000
1001 pub fn publish_batch(
1006 &self,
1007 topic: &str,
1008 messages: &[BatchMessage],
1009 ) -> Result<Vec<BatchResult>, Error> {
1010 let items: Vec<String> = messages.iter().map(BatchMessage::to_json).collect();
1011 let call = Call::new("POST", format!("/topics/{}/messages/batch", segment(topic)))
1012 .json_body(format!("{{\"messages\":[{}]}}", items.join(",")));
1013 BatchResult::parse_all(&self.call(&call)?.text())
1014 }
1015
1016 pub fn ack_batch(&self, topic: &str, messages: &[Message]) -> Result<Vec<BatchResult>, Error> {
1019 let call = Call::new("POST", format!("/topics/{}/messages/ack", segment(topic)))
1020 .json_body(receipts_json(messages, ""));
1021 BatchResult::parse_all(&self.call(&call)?.text())
1022 }
1023
1024 pub fn change_visibility_batch(
1027 &self,
1028 topic: &str,
1029 messages: &[Message],
1030 seconds: u32,
1031 ) -> Result<Vec<BatchResult>, Error> {
1032 let call = Call::new(
1033 "POST",
1034 format!("/topics/{}/messages/visibility", segment(topic)),
1035 )
1036 .json_body(receipts_json(
1037 messages,
1038 &format!(",\"visibilityTimeout\":{seconds}"),
1039 ));
1040 BatchResult::parse_all(&self.call(&call)?.text())
1041 }
1042
1043 pub fn consume(&self, topic: &str, options: &ConsumeOptions) -> Result<Vec<Message>, Error> {
1046 let call = self.consume_call(topic, options);
1047 let raw = self.call(&call)?.text();
1048 let f = Fields::parse(&raw).map_err(|e| bad_response("consumo", e))?;
1049 json::array_items(f.raw("messages").unwrap_or("[]"))
1050 .map_err(|e| bad_response("messages", e))?
1051 .into_iter()
1052 .map(Message::parse)
1053 .collect()
1054 }
1055
1056 pub fn consume_call(&self, topic: &str, options: &ConsumeOptions) -> Call {
1057 let mut call = Call::new("GET", format!("/topics/{}/messages", segment(topic)));
1058 if let Some(v) = options.max_messages {
1059 call = call.query("maxMessages", v);
1060 }
1061 if let Some(v) = options.visibility_timeout {
1062 call = call.query("visibilityTimeout", v);
1063 }
1064 if let Some(v) = options.wait_time {
1065 call = call.query("waitTime", v);
1066 }
1067 if options.auto_create {
1068 call = call.query("autoCreate", true);
1069 if let Some(v) = options.volatile {
1070 call = call.query("volatile", v);
1071 }
1072 if let Some(v) = options.idle_timeout {
1073 call = call.query("idleTimeout", v);
1074 }
1075 if let Some(v) = options.fifo {
1076 call = call.query("fifo", v);
1077 }
1078 for (k, v) in &options.tags {
1079 call = call.query(&format!("tag.{k}"), v);
1080 }
1081 }
1082 call.request_id = options.request_id.clone();
1083 call.timeout =
1084 Some(Duration::from_secs(u64::from(options.wait_time.unwrap_or(0))) + LONG_POLL_SLACK);
1085 call
1086 }
1087
1088 pub fn ack(&self, topic: &str, message_id: &str, receipt_token: &str) -> Result<(), Error> {
1090 let call = Call::new(
1091 "DELETE",
1092 format!(
1093 "/topics/{}/messages/{}",
1094 segment(topic),
1095 segment(message_id)
1096 ),
1097 )
1098 .query("receiptToken", receipt_token);
1099 self.call(&call).map(|_| ())
1100 }
1101
1102 pub fn change_visibility(
1104 &self,
1105 topic: &str,
1106 message_id: &str,
1107 receipt_token: &str,
1108 seconds: u32,
1109 ) -> Result<VisibilityResponse, Error> {
1110 let call = Call::new(
1111 "PUT",
1112 format!(
1113 "/topics/{}/messages/{}/visibility",
1114 segment(topic),
1115 segment(message_id)
1116 ),
1117 )
1118 .query("receiptToken", receipt_token)
1119 .json_body(format!("{{\"visibilityTimeout\":{seconds}}}"));
1120 let raw = self.call(&call)?.text();
1121 let f = Fields::parse(&raw).map_err(|e| bad_response("invisibilidade", e))?;
1122 Ok(VisibilityResponse {
1123 id: f.string("id").unwrap_or_default(),
1124 invisible_until: f.string("invisibleUntil").unwrap_or_default(),
1125 raw,
1126 })
1127 }
1128
1129 pub fn subscribe<H, S>(
1134 &self,
1135 topic: &str,
1136 options: &ConsumeOptions,
1137 mut stop: S,
1138 mut handler: H,
1139 ) -> Result<(), Error>
1140 where
1141 H: FnMut(&Message) -> Result<(), Box<dyn std::error::Error>>,
1142 S: FnMut() -> bool,
1143 {
1144 let mut options = options.clone();
1145 options.wait_time.get_or_insert(20);
1146 options.max_messages.get_or_insert(1);
1147 while !stop() {
1148 for message in self.consume(topic, &options)? {
1149 if handler(&message).is_ok() {
1150 match self.ack(topic, &message.id, &message.receipt_token) {
1151 Err(e) if e.status == 409 || e.status == 404 => {}
1154 other => other?,
1155 }
1156 }
1157 if stop() {
1158 return Ok(());
1159 }
1160 }
1161 }
1162 Ok(())
1163 }
1164}
1165
1166mod handlers;
1167pub use handlers::{
1168 ConsumeError, Delivery, HandlerError, DEFAULT_MAX_MESSAGES, DEFAULT_VISIBILITY_TIMEOUT,
1169 DEFAULT_WAIT_TIME,
1170};
1171
1172#[cfg(test)]
1173mod tests;