1use std::time::Duration;
5use taba_client::json::{self, Fields};
6
7use taba_client::Call;
8
9const LONG_ACTION: Duration = Duration::from_secs(180);
11
12const STATS_FIELDS: &str = "name,depth,publishRate,consumeRate,oldestAge,worstCondition,conditions";
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ApiCall {
18 Me,
19 Nodes,
20 ClusterStatus,
21 NodeMetrics(String),
22 NodeBackup(String),
23 DrainStatus(String),
24 DrainStart(String),
25 DrainCancel(String),
26 FlushNode(String),
27 FlushCluster,
28 StopCluster,
29 Tokens(Option<String>),
31 TokenCreate(String),
33 TokenUpdate(String, String),
35 TokenRevoke(String),
36 Topics(Option<String>),
38 TopicStats(Option<String>),
40 TopicCreate(String),
42 TopicUpdate(String, String),
44 TopicDelete(String),
45 TopicDetail(String),
47 Publish(PublishSpec),
49 Receive(String, u32, u32),
51 Ack(String, String, String),
53 Visibility(String, String, String, u32),
56 Prune(String),
58}
59
60pub const RECEIVE_MAX: u32 = 10;
62pub const RECEIVE_HOLD_SECS: u32 = 30;
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct PublishSpec {
68 pub topic: String,
69 pub body: Vec<u8>,
70 pub content_type: String,
71 pub group_id: Option<String>,
72 pub deduplication_id: Option<String>,
73}
74
75impl ApiCall {
76 pub fn to_call(&self) -> Call {
77 let mut call = self.route();
78 if matches!(
79 self,
80 ApiCall::FlushNode(_) | ApiCall::FlushCluster | ApiCall::StopCluster
81 ) {
82 call.timeout = Some(LONG_ACTION);
84 }
85 call
86 }
87
88 fn route(&self) -> Call {
89 let node = |id: &str, tail: &str| format!("/nodes/{}{tail}", encode_segment(id));
90 match self {
91 ApiCall::Me => Call::new("GET", "/auth/me"),
92 ApiCall::Nodes => Call::new("GET", "/nodes"),
93 ApiCall::ClusterStatus => Call::new("GET", "/cluster/status"),
94 ApiCall::NodeMetrics(id) => {
95 let mut call = Call::new("GET", node(id, "/metrics"));
96 call.headers
97 .push(("Accept".to_owned(), "text/plain".to_owned()));
98 call
99 }
100 ApiCall::NodeBackup(id) => Call::new("GET", node(id, "/backup")),
101 ApiCall::DrainStatus(id) => Call::new("GET", node(id, "/drain")),
102 ApiCall::DrainStart(id) => Call::new("POST", node(id, "/drain")),
103 ApiCall::DrainCancel(id) => Call::new("DELETE", node(id, "/drain")),
104 ApiCall::FlushNode(id) => Call::new("POST", node(id, "/backup/flush")),
105 ApiCall::FlushCluster => Call::new("POST", "/cluster/backup/flush"),
106 ApiCall::StopCluster => Call::new("POST", "/cluster/stop"),
107 ApiCall::Tokens(cursor) => {
108 let mut call = Call::new("GET", "/tokens").query("limit", 200);
109 if let Some(cursor) = cursor {
110 call = call.query("cursor", cursor);
111 }
112 call
113 }
114 ApiCall::TokenCreate(body) => Call::new("POST", "/tokens").json_body(body.clone()),
115 ApiCall::TokenUpdate(id, body) => {
116 Call::new("PATCH", format!("/tokens/{}", encode_segment(id)))
117 .json_body(body.clone())
118 }
119 ApiCall::TokenRevoke(id) => {
120 Call::new("DELETE", format!("/tokens/{}", encode_segment(id)))
121 }
122 ApiCall::Topics(cursor) => {
123 let mut call = Call::new("GET", "/topics").query("limit", 200);
124 if let Some(cursor) = cursor {
125 call = call.query("cursor", cursor);
126 }
127 call
128 }
129 ApiCall::TopicStats(cursor) => {
130 let mut call = Call::new("GET", "/stats/topics")
131 .query("limit", 200)
132 .query("fields", STATS_FIELDS);
133 if let Some(cursor) = cursor {
134 call = call.query("cursor", cursor);
135 }
136 call
137 }
138 ApiCall::TopicCreate(body) => Call::new("POST", "/topics").json_body(body.clone()),
139 ApiCall::TopicUpdate(name, body) => {
140 Call::new("PATCH", format!("/topics/{}", encode_segment(name)))
141 .json_body(body.clone())
142 }
143 ApiCall::TopicDelete(name) => {
144 Call::new("DELETE", format!("/topics/{}", encode_segment(name)))
145 }
146 ApiCall::TopicDetail(name) => {
147 Call::new("GET", format!("/stats/topics/{}", encode_segment(name)))
148 }
149 ApiCall::Publish(spec) => {
150 let mut call = Call::new(
151 "POST",
152 format!("/topics/{}/messages", encode_segment(&spec.topic)),
153 );
154 call.body = spec.body.clone();
155 call.content_type = Some(spec.content_type.clone());
156 if let Some(group) = &spec.group_id {
157 call = call.query("groupId", group);
158 }
159 if let Some(dedup) = &spec.deduplication_id {
160 call = call.query("deduplicationId", dedup);
161 }
162 call
163 }
164 ApiCall::Receive(name, hold, max) => {
165 Call::new("GET", format!("/topics/{}/messages", encode_segment(name)))
166 .query("maxMessages", max)
167 .query("visibilityTimeout", hold)
168 }
169 ApiCall::Ack(topic, id, receipt) => Call::new(
170 "DELETE",
171 format!(
172 "/topics/{}/messages/{}",
173 encode_segment(topic),
174 encode_segment(id)
175 ),
176 )
177 .query("receiptToken", receipt),
178 ApiCall::Visibility(topic, id, receipt, secs) => Call::new(
179 "PUT",
180 format!(
181 "/topics/{}/messages/{}/visibility",
182 encode_segment(topic),
183 encode_segment(id)
184 ),
185 )
186 .query("receiptToken", receipt)
187 .json_body(format!("{{\"visibilityTimeout\":{secs}}}")),
188 ApiCall::Prune(name) => Call::new(
189 "DELETE",
190 format!("/topics/{}/messages", encode_segment(name)),
191 ),
192 }
193 }
194
195 pub fn is_action(&self) -> bool {
197 matches!(
198 self,
199 ApiCall::DrainStart(_)
200 | ApiCall::DrainCancel(_)
201 | ApiCall::FlushNode(_)
202 | ApiCall::FlushCluster
203 | ApiCall::StopCluster
204 | ApiCall::TokenCreate(_)
205 | ApiCall::TokenUpdate(..)
206 | ApiCall::TokenRevoke(_)
207 | ApiCall::TopicCreate(_)
208 | ApiCall::TopicUpdate(..)
209 | ApiCall::TopicDelete(_)
210 | ApiCall::Publish(_)
211 | ApiCall::Receive(..)
212 | ApiCall::Ack(..)
213 | ApiCall::Visibility(..)
214 | ApiCall::Prune(_)
215 )
216 }
217}
218
219fn encode_segment(id: &str) -> String {
221 id.bytes()
222 .map(|b| {
223 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
224 (b as char).to_string()
225 } else {
226 format!("%{b:02X}")
227 }
228 })
229 .collect()
230}
231
232fn fields(text: &str) -> Result<Fields<'_>, String> {
233 Fields::parse(text).map_err(|e| e.to_string())
234}
235
236fn num(f: &Fields<'_>, name: &str) -> Option<f64> {
237 f.raw(name)?.parse().ok()
238}
239
240fn whole(f: &Fields<'_>, name: &str) -> Option<u64> {
241 num(f, name).filter(|n| *n >= 0.0).map(|n| n as u64)
242}
243
244fn count_of(f: &Fields<'_>, name: &str) -> u64 {
246 match f.raw(name) {
247 Some(raw) if raw.trim_start().starts_with('{') => {
248 json::object_fields(raw).map_or(0, |entries| entries.len() as u64)
249 }
250 Some(_) => whole(f, name).unwrap_or(0),
251 None => 0,
252 }
253}
254
255fn items<'a>(f: &Fields<'a>, name: &str) -> Vec<&'a str> {
256 f.raw(name)
257 .and_then(|raw| json::array_items(raw).ok())
258 .unwrap_or_default()
259}
260
261fn texts(f: &Fields<'_>, name: &str) -> Vec<String> {
262 items(f, name)
263 .into_iter()
264 .filter_map(|s| json::decode_string(s).ok())
265 .collect()
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Default)]
270pub struct Me {
271 pub auth_enabled: bool,
272 pub root: bool,
273 pub name: Option<String>,
274 pub permissions: Vec<String>,
275}
276
277impl Me {
278 pub fn is_root(&self) -> bool {
280 self.root || !self.auth_enabled
281 }
282
283 pub fn is_admin(&self) -> bool {
285 self.is_root() || self.permissions.iter().any(|p| p == "admin")
286 }
287
288 pub fn can(&self, permission: &str) -> bool {
290 let has = |name: &str| self.permissions.iter().any(|p| p == name);
291 self.is_root()
292 || has(permission)
293 || (matches!(permission, "create" | "publish" | "consume")
294 && (has("manage") || has("admin")))
295 || (permission == "manage" && has("admin"))
296 }
297}
298
299pub fn parse_me(text: &str) -> Result<Me, String> {
300 let f = fields(text)?;
301 Ok(Me {
302 auth_enabled: f.boolean("authEnabled").unwrap_or(true),
303 root: f.boolean("root").unwrap_or(false),
304 name: f.string("name"),
305 permissions: texts(&f, "permissions"),
306 })
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct Missing {
312 pub node_id: String,
313 pub reason: String,
314}
315
316fn parse_missing(f: &Fields<'_>) -> Vec<Missing> {
317 items(f, "missingNodes")
318 .into_iter()
319 .filter_map(|raw| {
320 let m = Fields::parse(raw).ok()?;
321 Some(Missing {
322 node_id: m.string("nodeId")?,
323 reason: m.string("reason").unwrap_or_default(),
324 })
325 })
326 .collect()
327}
328
329#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct NodeInfo {
332 pub node_id: String,
333 pub is_self: bool,
334 pub reachable: bool,
335 pub state: String,
336 pub drain_state: String,
337 pub version: String,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Default)]
341pub struct Nodes {
342 pub items: Vec<NodeInfo>,
343 pub partial: bool,
344 pub missing: Vec<Missing>,
345}
346
347pub fn parse_nodes(text: &str) -> Result<Nodes, String> {
348 let f = fields(text)?;
349 let list = items(&f, "items")
350 .into_iter()
351 .filter_map(|raw| {
352 let n = Fields::parse(raw).ok()?;
353 Some(NodeInfo {
354 node_id: n.string("nodeId")?,
355 is_self: n.boolean("self").unwrap_or(false),
356 reachable: n.boolean("reachable").unwrap_or(true),
357 state: n.string("state").unwrap_or_default(),
358 drain_state: n.string("drainState").unwrap_or_default(),
359 version: n.string("version").unwrap_or_default(),
360 })
361 })
362 .collect();
363 Ok(Nodes {
364 items: list,
365 partial: f.boolean("partial").unwrap_or(false),
366 missing: parse_missing(&f),
367 })
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
372pub enum Severity {
373 #[default]
374 Ok,
375 Warning,
376 Critical,
377}
378
379impl Severity {
380 pub fn parse(text: &str) -> Severity {
381 match text {
382 "critical" => Severity::Critical,
383 "warning" => Severity::Warning,
384 _ => Severity::Ok,
385 }
386 }
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
390pub struct Condition {
391 pub name: String,
392 pub severity: Severity,
393}
394
395#[derive(Debug, Clone, PartialEq, Default)]
397pub struct NodeStatus {
398 pub node_id: String,
399 pub reachable: bool,
400 pub cluster_name: Option<String>,
401 pub version: String,
402 pub mode: String,
403 pub state: String,
404 pub drain_state: String,
405 pub started_at: Option<String>,
406 pub memory_used: u64,
407 pub memory_limit: u64,
408 pub memory_messages: u64,
409 pub disk_messages: u64,
410 pub disk_free: u64,
411 pub quorum: bool,
412 pub channels: u64,
413 pub syncing_blocks: u64,
414 pub below_replication: u64,
415 pub below_quorum: u64,
416 pub frozen_nodes: u64,
417 pub active_requests: f64,
418 pub rate_limited: f64,
419 pub rtt_p95: Option<f64>,
420}
421
422impl NodeStatus {
423 pub fn memory_ratio(&self) -> f64 {
425 if self.memory_limit == 0 {
426 0.0
427 } else {
428 (self.memory_used as f64 / self.memory_limit as f64).clamp(0.0, 1.0)
429 }
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Default)]
434pub struct ClusterStatus {
435 pub collected_at: String,
436 pub partial: bool,
437 pub missing: Vec<Missing>,
438 pub worst: String,
439 pub conditions: Vec<Condition>,
440 pub members: Vec<(String, u64)>,
441 pub members_version: u64,
442 pub quorum: bool,
443 pub syncing_blocks: u64,
444 pub stopped: bool,
446 pub planned_stop: bool,
448 pub stopped_at: Option<String>,
449 pub nodes: Vec<NodeStatus>,
450}
451
452fn parse_node_status(node_id: &str, reachable: bool, raw: Option<&str>) -> NodeStatus {
453 let mut node = NodeStatus {
454 node_id: node_id.to_owned(),
455 reachable,
456 ..NodeStatus::default()
457 };
458 let Some(f) = raw.and_then(|r| Fields::parse(r).ok()) else {
459 return node;
460 };
461 node.cluster_name = f.string("clusterName");
462 node.version = f.string("version").unwrap_or_default();
463 node.mode = f.string("mode").unwrap_or_default();
464 node.state = f.string("state").unwrap_or_default();
465 node.drain_state = f.string("drainState").unwrap_or_default();
466 node.started_at = f.string("startedAt");
467 if let Some(s) = f.raw("storage").and_then(|r| Fields::parse(r).ok()) {
468 node.memory_used = whole(&s, "memoryUsedBytes").unwrap_or(0);
469 node.memory_limit = whole(&s, "memoryLimitBytes").unwrap_or(0);
470 node.memory_messages = whole(&s, "memoryMessages").unwrap_or(0);
471 node.disk_messages = whole(&s, "diskMessages").unwrap_or(0);
472 node.disk_free = whole(&s, "diskFreeBytes").unwrap_or(0);
473 }
474 if let Some(s) = f.raw("signals").and_then(|r| Fields::parse(r).ok()) {
475 node.quorum = s.boolean("quorumAvailable").unwrap_or(false);
476 node.channels = count_of(&s, "channels");
477 node.syncing_blocks = whole(&s, "syncingBlocks").unwrap_or(0);
478 node.below_replication = whole(&s, "blocksBelowReplication").unwrap_or(0);
479 node.below_quorum = whole(&s, "blocksBelowQuorum").unwrap_or(0);
480 node.frozen_nodes = whole(&s, "frozenNodes").unwrap_or(0);
481 }
482 if let Some(r) = f.raw("runtime").and_then(|r| Fields::parse(r).ok()) {
483 node.active_requests = num(&r, "activeRequests").unwrap_or(0.0);
484 node.rate_limited = num(&r, "rateLimitRejected").unwrap_or(0.0);
485 node.rtt_p95 = num(&r, "channelRttP95Seconds");
486 }
487 node
488}
489
490pub fn parse_cluster_status(text: &str) -> Result<ClusterStatus, String> {
491 let f = fields(text)?;
492 let conditions = items(&f, "conditions")
493 .into_iter()
494 .filter_map(|raw| {
495 let c = Fields::parse(raw).ok()?;
496 Some(Condition {
497 name: c.string("condition")?,
498 severity: Severity::parse(&c.string("severity").unwrap_or_default()),
499 })
500 })
501 .collect();
502 let mut status = ClusterStatus {
503 collected_at: f.string("collectedAt").unwrap_or_default(),
504 partial: f.boolean("partial").unwrap_or(false),
505 missing: parse_missing(&f),
506 worst: f
507 .string("worstCondition")
508 .unwrap_or_else(|| "ok".to_owned()),
509 conditions,
510 ..ClusterStatus::default()
511 };
512 if let Some(s) = f.raw("summary").and_then(|r| Fields::parse(r).ok()) {
513 status.members_version = whole(&s, "membersVersion").unwrap_or(0);
514 status.quorum = s.boolean("quorumAvailable").unwrap_or(false);
515 status.syncing_blocks = whole(&s, "syncingBlocks").unwrap_or(0);
516 status.stopped = s.boolean("stopped").unwrap_or(false);
517 status.planned_stop = s.boolean("plannedStop").unwrap_or(false);
518 status.stopped_at = s.string("stoppedAt");
519 if let Some(m) = s.raw("members").and_then(|r| json::object_fields(r).ok()) {
520 status.members = m
521 .into_iter()
522 .map(|(state, n)| (state, n.parse().unwrap_or(0)))
523 .collect();
524 }
525 }
526 status.nodes = items(&f, "nodes")
527 .into_iter()
528 .filter_map(|raw| {
529 let n = Fields::parse(raw).ok()?;
530 let id = n.string("nodeId")?;
531 let reachable = n.boolean("reachable").unwrap_or(false);
532 Some(parse_node_status(&id, reachable, n.raw("status")))
533 })
534 .collect();
535 Ok(status)
536}
537
538#[derive(Debug, Clone, PartialEq, Eq)]
540pub struct Drain {
541 pub state: String,
542 pub pending: u64,
543}
544
545impl Drain {
546 pub fn is_active(&self) -> bool {
547 self.state == "active"
548 }
549
550 pub fn is_done(&self) -> bool {
551 self.state == "drained"
552 }
553}
554
555pub fn parse_drain(text: &str) -> Result<Drain, String> {
556 let f = fields(text)?;
557 Ok(Drain {
558 state: f.string("drainState").unwrap_or_default(),
559 pending: whole(&f, "pendingMessages").unwrap_or(0),
560 })
561}
562
563#[derive(Debug, Clone, PartialEq, Eq, Default)]
565pub struct Backup {
566 pub reachable: Option<bool>,
567 pub pending: Option<u64>,
568 pub last_success: Option<String>,
569 pub segments: Option<u64>,
570}
571
572pub fn parse_backup(text: &str) -> Result<Backup, String> {
573 let f = fields(text)?;
574 Ok(Backup {
575 reachable: f.boolean("s3Reachable").or_else(|| f.boolean("reachable")),
576 pending: whole(&f, "outboxPending").or_else(|| whole(&f, "pending")),
577 last_success: f
578 .string("lastSuccessAt")
579 .or_else(|| f.string("lastSuccess")),
580 segments: whole(&f, "segments").or_else(|| whole(&f, "segmentsUploaded")),
581 })
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Default)]
586pub struct FlushNode {
587 pub node_id: Option<String>,
588 pub complete: bool,
589 pub pending: bool,
590 pub timed_out: bool,
591 pub outbox_pending: u64,
592 pub memory_remaining: u64,
593 pub error: Option<String>,
594}
595
596#[derive(Debug, Clone, PartialEq, Eq, Default)]
597pub struct FlushReport {
598 pub complete: bool,
599 pub nodes: Vec<FlushNode>,
600}
601
602fn flush_node(f: &Fields<'_>) -> FlushNode {
603 FlushNode {
604 node_id: f.string("nodeId"),
605 complete: f.boolean("complete").unwrap_or(false),
606 pending: f.boolean("pending").unwrap_or(false),
607 timed_out: f.boolean("timedOut").unwrap_or(false),
608 outbox_pending: whole(f, "outboxPending").unwrap_or(0),
609 memory_remaining: whole(f, "memoryRemaining").unwrap_or(0),
610 error: f.string("error"),
611 }
612}
613
614pub fn parse_flush(text: &str) -> Result<FlushReport, String> {
616 let f = fields(text)?;
617 let complete = f.boolean("complete").unwrap_or(false);
618 let nodes = if f.raw("nodes").is_some() {
619 items(&f, "nodes")
620 .into_iter()
621 .filter_map(|raw| Fields::parse(raw).ok().map(|n| flush_node(&n)))
622 .collect()
623 } else {
624 vec![flush_node(&f)]
625 };
626 Ok(FlushReport { complete, nodes })
627}
628
629#[derive(Debug, Clone, PartialEq, Eq, Default)]
630pub struct StopReport {
631 pub stopped: Vec<String>,
632 pub stop_list: bool,
633 pub warnings: Vec<String>,
634 pub nodes: Vec<(String, bool)>,
635}
636
637pub fn parse_stop(text: &str) -> Result<StopReport, String> {
638 let f = fields(text)?;
639 let nodes = items(&f, "nodes")
640 .into_iter()
641 .filter_map(|raw| {
642 let n = Fields::parse(raw).ok()?;
643 Some((n.string("nodeId")?, n.boolean("drained").unwrap_or(false)))
644 })
645 .collect();
646 Ok(StopReport {
647 stopped: texts(&f, "stopped"),
648 stop_list: f.boolean("stopList").unwrap_or(false),
649 warnings: texts(&f, "warnings"),
650 nodes,
651 })
652}
653
654#[derive(Debug, Clone, PartialEq, Eq, Default)]
656pub struct Token {
657 pub id: String,
658 pub name: String,
659 pub permissions: Vec<String>,
660 pub prefixes: Vec<String>,
661 pub created_at: Option<String>,
662 pub expires_at: Option<String>,
663}
664
665fn token_of(f: &Fields<'_>) -> Option<Token> {
666 Some(Token {
667 id: f.string("id")?,
668 name: f.string("name").unwrap_or_default(),
669 permissions: texts(f, "permissions"),
670 prefixes: texts(f, "topicPrefixes"),
671 created_at: f.string("createdAt"),
672 expires_at: f.string("expiresAt"),
673 })
674}
675
676#[derive(Debug, Clone, PartialEq, Eq, Default)]
678pub struct TokenPage {
679 pub items: Vec<Token>,
680 pub next_cursor: Option<String>,
681}
682
683pub fn parse_tokens(text: &str) -> Result<TokenPage, String> {
684 let f = fields(text)?;
685 let list = items(&f, "items")
686 .into_iter()
687 .filter_map(|raw| token_of(&Fields::parse(raw).ok()?))
688 .collect();
689 Ok(TokenPage {
690 items: list,
691 next_cursor: f.string("nextCursor"),
692 })
693}
694
695pub fn parse_token_created(text: &str) -> Result<(Token, String), String> {
697 let f = fields(text)?;
698 let token = token_of(&f).ok_or_else(|| "the reply has no token id".to_owned())?;
699 let secret = f
700 .string("token")
701 .ok_or_else(|| "the reply has no secret".to_owned())?;
702 Ok((token, secret))
703}
704
705#[derive(Debug, Clone, PartialEq, Eq, Default)]
707pub struct Topic {
708 pub id: String,
709 pub name: String,
710 pub kind: String,
712 pub fifo: bool,
713 pub volatile: bool,
714 pub tags: Vec<(String, String)>,
715 pub prefixes: Vec<String>,
716 pub idle_timeout: Option<u64>,
717 pub max_message_bytes: Option<u64>,
719 pub ttl: Option<u64>,
720 pub max_deliveries: Option<u64>,
721 pub effective_max_message_bytes: Option<u64>,
723 pub effective_ttl: Option<u64>,
724 pub effective_max_deliveries: Option<u64>,
725 pub dead_letter: bool,
726 pub source: Option<String>,
728 pub created_at: Option<String>,
729}
730
731impl Topic {
732 pub fn is_distributor(&self) -> bool {
733 self.kind == "distributor"
734 }
735}
736
737fn pairs_of(f: &Fields<'_>, name: &str) -> Vec<(String, String)> {
738 f.raw(name)
739 .and_then(|raw| json::object_fields(raw).ok())
740 .unwrap_or_default()
741 .into_iter()
742 .filter_map(|(key, value)| Some((key, json::decode_string(value).ok()?)))
743 .collect()
744}
745
746fn topic_of(f: &Fields<'_>) -> Option<Topic> {
747 let effective = f.raw("effective").and_then(|r| Fields::parse(r).ok());
748 let own = |name: &str| effective.as_ref().and_then(|e| whole(e, name));
749 Some(Topic {
750 id: f.string("id").unwrap_or_default(),
751 name: f.string("name")?,
752 kind: f.string("type").unwrap_or_else(|| "queue".to_owned()),
753 fifo: f.boolean("fifo").unwrap_or(false),
754 volatile: f.boolean("volatile").unwrap_or(false),
755 tags: pairs_of(f, "tags"),
756 prefixes: texts(f, "prefixes"),
757 idle_timeout: whole(f, "idleTimeout"),
758 max_message_bytes: whole(f, "maxMessageBytes"),
759 ttl: whole(f, "ttl"),
760 max_deliveries: whole(f, "maxDeliveries"),
761 effective_max_message_bytes: own("maxMessageBytes"),
762 effective_ttl: own("ttl"),
763 effective_max_deliveries: own("maxDeliveries"),
764 dead_letter: f.boolean("deadLetter").unwrap_or(false),
765 source: f.string("source"),
766 created_at: f.string("createdAt"),
767 })
768}
769
770#[derive(Debug, Clone, PartialEq, Eq, Default)]
772pub struct TopicPage {
773 pub items: Vec<Topic>,
774 pub next_cursor: Option<String>,
775}
776
777pub fn parse_topics(text: &str) -> Result<TopicPage, String> {
778 let f = fields(text)?;
779 let list = items(&f, "items")
780 .into_iter()
781 .filter_map(|raw| topic_of(&Fields::parse(raw).ok()?))
782 .collect();
783 Ok(TopicPage {
784 items: list,
785 next_cursor: f.string("nextCursor"),
786 })
787}
788
789pub fn parse_topic(text: &str) -> Result<Topic, String> {
791 topic_of(&fields(text)?).ok_or_else(|| "the reply has no topic name".to_owned())
792}
793
794#[derive(Debug, Clone, PartialEq, Default)]
796pub struct TopicNumbers {
797 pub depth: Option<u64>,
798 pub publish_rate: Option<f64>,
799 pub consume_rate: Option<f64>,
800 pub oldest_age: Option<f64>,
802 pub worst: String,
804 pub severity: Severity,
805}
806
807#[derive(Debug, Clone, PartialEq, Default)]
809pub struct TopicStatsPage {
810 pub items: Vec<(String, TopicNumbers)>,
811 pub next_cursor: Option<String>,
812 pub partial: bool,
813 pub missing: Vec<Missing>,
814}
815
816pub fn parse_topic_stats(text: &str) -> Result<TopicStatsPage, String> {
817 let f = fields(text)?;
818 let list = items(&f, "items")
819 .into_iter()
820 .filter_map(|raw| {
821 let t = Fields::parse(raw).ok()?;
822 let severity = items(&t, "conditions")
823 .first()
824 .and_then(|c| Fields::parse(c).ok())
825 .and_then(|c| c.string("severity"))
826 .map_or(Severity::Ok, |s| Severity::parse(&s));
827 Some((
828 t.string("name")?,
829 TopicNumbers {
830 depth: whole(&t, "depth"),
831 publish_rate: num(&t, "publishRate"),
832 consume_rate: num(&t, "consumeRate"),
833 oldest_age: num(&t, "oldestAge"),
834 worst: t
835 .string("worstCondition")
836 .unwrap_or_else(|| "ok".to_owned()),
837 severity,
838 },
839 ))
840 })
841 .collect();
842 Ok(TopicStatsPage {
843 items: list,
844 next_cursor: f.string("nextCursor"),
845 partial: f.boolean("partial").unwrap_or(false),
846 missing: parse_missing(&f),
847 })
848}
849
850#[derive(Debug, Clone, PartialEq, Default)]
853pub struct TopicSample {
854 pub visible: u64,
855 pub invisible: u64,
856 pub publish_rate: f64,
858 pub consume_rate: f64,
859 pub ack_rate: f64,
860 pub oldest_age: Option<f64>,
862 pub groups: Vec<(String, f64)>,
864 pub partial: bool,
865}
866
867pub fn parse_topic_sample(text: &str) -> Result<TopicSample, String> {
868 let f = fields(text)?;
869 let groups = items(&f, "topGroups")
870 .into_iter()
871 .filter_map(|raw| {
872 let g = Fields::parse(raw).ok()?;
873 Some((g.string("groupId")?, num(&g, "oldestAge")?))
874 })
875 .collect();
876 Ok(TopicSample {
877 visible: whole(&f, "visibleMessages").unwrap_or(0),
878 invisible: whole(&f, "invisibleMessages").unwrap_or(0),
879 publish_rate: num(&f, "publishRate").unwrap_or(0.0),
880 consume_rate: num(&f, "consumeRate").unwrap_or(0.0),
881 ack_rate: num(&f, "ackRate").unwrap_or(0.0),
882 oldest_age: num(&f, "oldestAge"),
883 groups,
884 partial: f.boolean("partial").unwrap_or(false),
885 })
886}
887
888pub fn parse_received(text: &str) -> Result<Vec<taba_client::Message>, String> {
890 let f = fields(text)?;
891 items(&f, "messages")
892 .into_iter()
893 .map(|raw| taba_client::Message::parse(raw).map_err(|e| e.to_string()))
894 .collect()
895}
896
897#[derive(Debug, Clone, PartialEq, Eq, Default)]
900pub struct Published {
901 pub id: Option<String>,
902 pub duplicate: bool,
903 pub matched: Option<u64>,
904}
905
906pub fn parse_published(text: &str) -> Result<Published, String> {
907 let f = fields(text)?;
908 Ok(Published {
909 id: f.string("id"),
910 duplicate: f.boolean("duplicate").unwrap_or(false),
911 matched: whole(&f, "matched"),
912 })
913}
914
915pub fn parse_pruned(text: &str) -> Result<u64, String> {
917 let f = fields(text)?;
918 Ok(whole(&f, "removed").unwrap_or(0))
919}
920
921#[cfg(test)]
922mod tests;