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![
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/// A `PATCH` field that accepts going back to the server default (`null`).
716#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
717pub enum Nullable<T> {
718    /// Does not send the field.
719    #[default]
720    Keep,
721    /// Sends `null`: goes back to the server default.
722    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/// `PATCH /topics/{topic}`: only what was filled in is sent.
737#[derive(Debug, Clone, Default)]
738pub struct TopicUpdate {
739    /// Replaces the whole map (`Some(empty)` clears the tags).
740    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    /// Default `application/json`.
770    pub content_type: Option<String>,
771    pub group_id: Option<String>,
772    pub deduplication_id: Option<String>,
773    pub request_id: Option<String>,
774    /// Set it when the topic is a FIFO queue: only there can `502`/`504` be retried.
775    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    /// The next four only apply with `auto_create`.
785    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
796// ---------------------------------------------------------------------------------------------
797// The nine operations
798// ---------------------------------------------------------------------------------------------
799
800impl Client {
801    /// `POST /topics`.
802    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    /// `GET /topics/{topic}`.
813    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    /// `GET /topics`. Parameters that are `None` do not go in the query.
819    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    /// All topics, page by page.
839    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    /// `PATCH /topics/{topic}`.
853    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    /// `DELETE /topics/{topic}`.
860    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    /// `POST /topics/{topic}/messages`. The body goes as bytes, without validation.
867    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        // A publication with an empty body still needs a `Content-Type`: the body goes as it came.
890        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    /// `POST /topics/{topic}/messages/batch`: publishes many messages in one request (1 to the server's
913    /// `api.batch.max-messages`, 1000 by default). Each item is answered as its single publication and one that fails
914    /// does not undo the others (see [`BatchResult::is_ok`]). On a FIFO queue the items of a group are published in
915    /// the order of the list. A failure of the network is not retried (it could duplicate).
916    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    /// `POST /topics/{topic}/messages/ack`: acknowledges many delivered messages in one request. Each item is answered
928    /// as its single ack and one that fails does not stop the others.
929    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    /// `POST /topics/{topic}/messages/visibility`: sets the invisibility of many delivered messages to `seconds` from
936    /// now, in one request.
937    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    /// `GET /topics/{topic}/messages`. The read time is `waitTime + 10 s`, not the
955    /// `requestTimeout`.
956    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    /// `DELETE /topics/{topic}/messages/{id}?receiptToken=`.
1000    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    /// `PUT /topics/{topic}/messages/{id}/visibility?receiptToken=`.
1014    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    /// The consume loop (D71.1): long polling (`waitTime` 20 and one message at a time, if not
1041    /// set), calls the `handler` and acknowledges by itself **only if it returns `Ok`**. `stop` is
1042    /// checked before each consume and after each message; returning `true`, the loop ends.
1043    /// A server error while consuming or acknowledging propagates.
1044    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                        // The message already went back to the queue (deadline expired, `409`) or is already gone (`404`):
1063                        // there is nothing to acknowledge, and the loop goes on.
1064                        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;