Skip to main content

taba_client/client/
mod.rs

1//! The client: the configuration, the call with the common contract's retries (doc 19, section 18.4) and the
2//! nine operations of the surface (section 18.3).
3
4use 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
13/// The version of the API contract this connector implements (UC-CNT-028).
14pub const CONTRACT_VERSION: &str = "1";
15
16/// Above this, `consume` waits no more than the `waitTime` plus the slack below.
17const LONG_POLL_SLACK: Duration = Duration::from_secs(10);
18
19#[derive(Debug, Clone)]
20pub struct Config {
21    /// Address of the client API (`http://host:port` or `https://...`).
22    pub base_url: String,
23    pub token: Option<String>,
24    pub connect_timeout: Duration,
25    pub request_timeout: Duration,
26    /// Attempts, counting the first.
27    pub max_attempts: u32,
28    pub user_agent: String,
29    /// CA certificate (PEM) to trust, besides the system roots.
30    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/// The only kind of error of the connector: the API `ErrorResponse`, or a failure of the client itself
48/// (`status` zero and `code` `client.network`, `client.timeout` or `client.invalid-config`).
49#[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    /// Seconds of the `Retry-After`, if it came.
56    pub retry_after: Option<u64>,
57    /// The `details` list of the `ErrorResponse`, as JSON text.
58    pub details: Option<String>,
59    /// The response body, as it came (empty on a client failure).
60    pub body: Vec<u8>,
61}
62
63impl Error {
64    /// An error of the client itself (`status` zero), for whoever uses the connector to build their own.
65    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    /// A failure of the client itself, not a server response.
82    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/// A call to the API, before it becomes HTTP.
100#[derive(Debug, Clone)]
101pub struct Call {
102    pub method: &'static str,
103    /// Path with the segments already encoded (`/topics/orders`).
104    pub path: String,
105    pub query: Vec<(String, String)>,
106    pub body: Vec<u8>,
107    /// Only applies with a body; default `application/json`.
108    /// Extra headers (an `Accept` of its own replaces the default `application/json`).
109    pub headers: Vec<(String, String)>,
110    pub content_type: Option<String>,
111    pub request_id: Option<String>,
112    pub traceparent: Option<String>,
113    /// Overrides the total time of the call (`consume` uses `waitTime + 10 s`).
114    pub timeout: Option<Duration>,
115    /// Retries `502` and `504`: only publishing to a FIFO queue, where the primary deduplicates.
116    pub retry_gateway: bool,
117    /// After a `503`, these responses count as success (the operation had already been applied):
118    /// creating a topic (`409 topic.already-exists`) and deleting (`404`).
119    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/// The response of a call that succeeded.
151#[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    /// `true` when the response is a `409`/`404` accepted as success after a `503` (the operation
158    /// had been applied on the previous attempt).
159    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    /// To swap the network stack (and, in tests, the wait between attempts).
184    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    /// Makes the call, retrying only what the contract says to retry (section 18.4):
240    /// `429` and `503` (waiting for the `Retry-After`, or 1 s, 2 s, 4 s), `502` and `504` only with
241    /// `retry_gateway`, and a network failure **before** sending. Never a `4xx` (except `429`), nor
242    /// a failure after sending the body, nor a timeout.
243    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
337/// The response's `ErrorResponse`; if the body is not one, the error carries the code `http.<status>`.
338fn 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
373// ---------------------------------------------------------------------------------------------
374// Modelos
375// ---------------------------------------------------------------------------------------------
376
377fn 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    /// `queue` or `distributor`.
386    pub topic_type: String,
387    pub created_at: String,
388    pub fifo: Option<bool>,
389    pub volatile: Option<bool>,
390    /// The topic JSON, as the server returned it.
391    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/// A page (`Page<T>`): the items and the cursor of the next one, if any.
410#[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/// A delivered message. The `body` always comes out as bytes (section 18.4).
433#[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    /// The message JSON, as the server returned it.
447    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    /// The message `id` (queue). Absent from a distributor's response.
500    pub id: Option<String>,
501    /// FIFO queue only.
502    pub duplicate: Option<bool>,
503    /// Distributor only: how many queues matched.
504    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/// One message of [`Client::publish_batch`]: the same fields as [`Client::publish`]. The bytes travel in base64,
516/// so any type fits.
517#[derive(Debug, Clone, PartialEq, Eq)]
518pub struct BatchMessage {
519    pub body: Vec<u8>,
520    /// What the bytes are (`None`: `application/json`).
521    pub content_type: Option<String>,
522    /// Required on a FIFO queue; elsewhere it guides the fair delivery.
523    pub group_id: Option<String>,
524    /// FIFO queues only: the deduplication key inside the group.
525    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    /// A text (`text/plain; charset=utf-8`).
539    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/// The result of one item of a batch, in the order of the request: the `status` the single call would have answered
578/// and what it would have returned. An item that failed is not an `Err`: `error_code` says why.
579#[derive(Debug, Clone, PartialEq, Eq)]
580pub struct BatchResult {
581    /// `201` published, `200` a FIFO duplicate, a distributor or a new invisibility, `204` acked, or the error's.
582    pub status: u16,
583    pub id: Option<String>,
584    /// FIFO queue only.
585    pub duplicate: Option<bool>,
586    /// Distributor only: how many queues matched.
587    pub matched: Option<u64>,
588    /// `change_visibility_batch` only: the new deadline.
589    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    /// The item was applied (no error).
597    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
632/// The items of an ack or a change of invisibility in batch: `id` and `receiptToken` of each message, plus `extra`.
633fn 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
647// ---------------------------------------------------------------------------------------------
648// Requests
649// ---------------------------------------------------------------------------------------------
650
651fn 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/// `POST /topics`. The connector sends only what was filled in; whatever does not apply to the type, the server
665/// refuses.
666#[derive(Debug, Clone, Default)]
667pub struct TopicRequest {
668    pub name: String,
669    /// `queue` or `distributor`.
670    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    /// The body of `PUT /topics/{topic}`: the same fields without `name` (it goes in the path).
689    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/// What `create_or_update_topic` does when the topic exists with another `type`, `fifo` or `volatile`
725/// (fields that do not change in place).
726#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
727pub enum Recreate {
728    /// Refuses with `409 topic.immutable-mismatch`; nothing changes.
729    #[default]
730    Never,
731    /// Removes and creates it again only if the queue and its dead-letter hold no message on any node
732    /// (`409 topic.not-empty` otherwise).
733    IfEmpty,
734    /// Removes it with its messages and dead-letter and creates it again (new `id`).
735    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/// What `PUT /topics/{topic}` did.
749#[derive(Debug, Clone, Copy, PartialEq, Eq)]
750pub enum TopicOutcome {
751    /// It did not exist (`201`).
752    Created,
753    /// It existed: the mutable fields are now the declared ones, or already were (`200`).
754    Updated,
755    /// It was removed and created again with a new `id` (`201` with `Taba-Topic-Recreated: true`).
756    Recreated,
757}
758
759/// The answer of `create_or_update_topic`: the topic as it is now and what was done.
760#[derive(Debug, Clone, PartialEq, Eq)]
761pub struct TopicPut {
762    pub topic: Topic,
763    pub outcome: TopicOutcome,
764}
765
766/// A `PATCH` field that accepts going back to the server default (`null`).
767#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
768pub enum Nullable<T> {
769    /// Does not send the field.
770    #[default]
771    Keep,
772    /// Sends `null`: goes back to the server default.
773    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/// `PATCH /topics/{topic}`: only what was filled in is sent.
788#[derive(Debug, Clone, Default)]
789pub struct TopicUpdate {
790    /// Replaces the whole map (`Some(empty)` clears the tags).
791    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    /// Default `application/json`.
821    pub content_type: Option<String>,
822    pub group_id: Option<String>,
823    pub deduplication_id: Option<String>,
824    pub request_id: Option<String>,
825    /// Set it when the topic is a FIFO queue: only there can `502`/`504` be retried.
826    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    /// The next four only apply with `auto_create`.
836    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
847// ---------------------------------------------------------------------------------------------
848// The nine operations
849// ---------------------------------------------------------------------------------------------
850
851impl Client {
852    /// `POST /topics`.
853    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    /// `GET /topics/{topic}`.
864    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    /// `GET /topics`. Parameters that are `None` do not go in the query.
870    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    /// All topics, page by page.
890    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    /// `PATCH /topics/{topic}`.
904    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    /// `PUT /topics/{topic}`: brings the topic to the declared state, creating it if it does not exist. What is
911    /// left out goes back to the server default (a full replacement, unlike `update_topic`); `request.name` is the
912    /// topic of the path. Another `type`, `fifo` or `volatile` is `409 topic.immutable-mismatch` unless `recreate`
913    /// says otherwise. Repeated after a `503` like the other calls: the route is idempotent.
914    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    /// `DELETE /topics/{topic}`.
940    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    /// `DELETE /topics/{topic}?ifEmpty=true`: removes the queue only if it and its dead-letter hold no message on
947    /// any node (`409 topic.not-empty` otherwise; a distributor holds none).
948    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    /// `POST /topics/{topic}/messages`. The body goes as bytes, without validation.
956    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        // A publication with an empty body still needs a `Content-Type`: the body goes as it came.
979        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    /// `POST /topics/{topic}/messages/batch`: publishes many messages in one request (1 to the server's
1002    /// `api.batch.max-messages`, 1000 by default). Each item is answered as its single publication and one that fails
1003    /// does not undo the others (see [`BatchResult::is_ok`]). On a FIFO queue the items of a group are published in
1004    /// the order of the list. A failure of the network is not retried (it could duplicate).
1005    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    /// `POST /topics/{topic}/messages/ack`: acknowledges many delivered messages in one request. Each item is answered
1017    /// as its single ack and one that fails does not stop the others.
1018    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    /// `POST /topics/{topic}/messages/visibility`: sets the invisibility of many delivered messages to `seconds` from
1025    /// now, in one request.
1026    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    /// `GET /topics/{topic}/messages`. The read time is `waitTime + 10 s`, not the
1044    /// `requestTimeout`.
1045    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    /// `DELETE /topics/{topic}/messages/{id}?receiptToken=`.
1089    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    /// `PUT /topics/{topic}/messages/{id}/visibility?receiptToken=`.
1103    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    /// The consume loop (D71.1): long polling (`waitTime` 20 and one message at a time, if not
1130    /// set), calls the `handler` and acknowledges by itself **only if it returns `Ok`**. `stop` is
1131    /// checked before each consume and after each message; returning `true`, the loop ends.
1132    /// A server error while consuming or acknowledging propagates.
1133    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                        // The message already went back to the queue (deadline expired, `409`) or is already gone (`404`):
1152                        // there is nothing to acknowledge, and the loop goes on.
1153                        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;