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 state: String,
403 pub drain_state: String,
404 pub started_at: Option<String>,
405 pub memory_used: u64,
406 pub memory_limit: u64,
407 pub memory_messages: u64,
408 pub disk_messages: u64,
409 pub disk_free: u64,
410 pub quorum: bool,
411 pub channels: u64,
412 pub syncing_blocks: u64,
413 pub below_replication: u64,
414 pub below_quorum: u64,
415 pub frozen_nodes: u64,
416 pub active_requests: f64,
417 pub rate_limited: f64,
418 pub rtt_p95: Option<f64>,
419}
420
421impl NodeStatus {
422 pub fn memory_ratio(&self) -> f64 {
424 if self.memory_limit == 0 {
425 0.0
426 } else {
427 (self.memory_used as f64 / self.memory_limit as f64).clamp(0.0, 1.0)
428 }
429 }
430}
431
432#[derive(Debug, Clone, PartialEq, Default)]
433pub struct ClusterStatus {
434 pub collected_at: String,
435 pub partial: bool,
436 pub missing: Vec<Missing>,
437 pub worst: String,
438 pub conditions: Vec<Condition>,
439 pub members: Vec<(String, u64)>,
440 pub members_version: u64,
441 pub quorum: bool,
442 pub syncing_blocks: u64,
443 pub stopped: bool,
445 pub planned_stop: bool,
447 pub stopped_at: Option<String>,
448 pub nodes: Vec<NodeStatus>,
449}
450
451fn parse_node_status(node_id: &str, reachable: bool, raw: Option<&str>) -> NodeStatus {
452 let mut node = NodeStatus {
453 node_id: node_id.to_owned(),
454 reachable,
455 ..NodeStatus::default()
456 };
457 let Some(f) = raw.and_then(|r| Fields::parse(r).ok()) else {
458 return node;
459 };
460 node.cluster_name = f.string("clusterName");
461 node.version = f.string("version").unwrap_or_default();
462 node.state = f.string("state").unwrap_or_default();
463 node.drain_state = f.string("drainState").unwrap_or_default();
464 node.started_at = f.string("startedAt");
465 if let Some(s) = f.raw("storage").and_then(|r| Fields::parse(r).ok()) {
466 node.memory_used = whole(&s, "memoryUsedBytes").unwrap_or(0);
467 node.memory_limit = whole(&s, "memoryLimitBytes").unwrap_or(0);
468 node.memory_messages = whole(&s, "memoryMessages").unwrap_or(0);
469 node.disk_messages = whole(&s, "diskMessages").unwrap_or(0);
470 node.disk_free = whole(&s, "diskFreeBytes").unwrap_or(0);
471 }
472 if let Some(s) = f.raw("signals").and_then(|r| Fields::parse(r).ok()) {
473 node.quorum = s.boolean("quorumAvailable").unwrap_or(false);
474 node.channels = count_of(&s, "channels");
475 node.syncing_blocks = whole(&s, "syncingBlocks").unwrap_or(0);
476 node.below_replication = whole(&s, "blocksBelowReplication").unwrap_or(0);
477 node.below_quorum = whole(&s, "blocksBelowQuorum").unwrap_or(0);
478 node.frozen_nodes = whole(&s, "frozenNodes").unwrap_or(0);
479 }
480 if let Some(r) = f.raw("runtime").and_then(|r| Fields::parse(r).ok()) {
481 node.active_requests = num(&r, "activeRequests").unwrap_or(0.0);
482 node.rate_limited = num(&r, "rateLimitRejected").unwrap_or(0.0);
483 node.rtt_p95 = num(&r, "channelRttP95Seconds");
484 }
485 node
486}
487
488pub fn parse_cluster_status(text: &str) -> Result<ClusterStatus, String> {
489 let f = fields(text)?;
490 let conditions = items(&f, "conditions")
491 .into_iter()
492 .filter_map(|raw| {
493 let c = Fields::parse(raw).ok()?;
494 Some(Condition {
495 name: c.string("condition")?,
496 severity: Severity::parse(&c.string("severity").unwrap_or_default()),
497 })
498 })
499 .collect();
500 let mut status = ClusterStatus {
501 collected_at: f.string("collectedAt").unwrap_or_default(),
502 partial: f.boolean("partial").unwrap_or(false),
503 missing: parse_missing(&f),
504 worst: f
505 .string("worstCondition")
506 .unwrap_or_else(|| "ok".to_owned()),
507 conditions,
508 ..ClusterStatus::default()
509 };
510 if let Some(s) = f.raw("summary").and_then(|r| Fields::parse(r).ok()) {
511 status.members_version = whole(&s, "membersVersion").unwrap_or(0);
512 status.quorum = s.boolean("quorumAvailable").unwrap_or(false);
513 status.syncing_blocks = whole(&s, "syncingBlocks").unwrap_or(0);
514 status.stopped = s.boolean("stopped").unwrap_or(false);
515 status.planned_stop = s.boolean("plannedStop").unwrap_or(false);
516 status.stopped_at = s.string("stoppedAt");
517 if let Some(m) = s.raw("members").and_then(|r| json::object_fields(r).ok()) {
518 status.members = m
519 .into_iter()
520 .map(|(state, n)| (state, n.parse().unwrap_or(0)))
521 .collect();
522 }
523 }
524 status.nodes = items(&f, "nodes")
525 .into_iter()
526 .filter_map(|raw| {
527 let n = Fields::parse(raw).ok()?;
528 let id = n.string("nodeId")?;
529 let reachable = n.boolean("reachable").unwrap_or(false);
530 Some(parse_node_status(&id, reachable, n.raw("status")))
531 })
532 .collect();
533 Ok(status)
534}
535
536#[derive(Debug, Clone, PartialEq, Eq)]
538pub struct Drain {
539 pub state: String,
540 pub pending: u64,
541}
542
543impl Drain {
544 pub fn is_active(&self) -> bool {
545 self.state == "active"
546 }
547
548 pub fn is_done(&self) -> bool {
549 self.state == "drained"
550 }
551}
552
553pub fn parse_drain(text: &str) -> Result<Drain, String> {
554 let f = fields(text)?;
555 Ok(Drain {
556 state: f.string("drainState").unwrap_or_default(),
557 pending: whole(&f, "pendingMessages").unwrap_or(0),
558 })
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Default)]
563pub struct Backup {
564 pub reachable: Option<bool>,
565 pub pending: Option<u64>,
566 pub last_success: Option<String>,
567 pub segments: Option<u64>,
568}
569
570pub fn parse_backup(text: &str) -> Result<Backup, String> {
571 let f = fields(text)?;
572 Ok(Backup {
573 reachable: f.boolean("s3Reachable").or_else(|| f.boolean("reachable")),
574 pending: whole(&f, "outboxPending").or_else(|| whole(&f, "pending")),
575 last_success: f
576 .string("lastSuccessAt")
577 .or_else(|| f.string("lastSuccess")),
578 segments: whole(&f, "segments").or_else(|| whole(&f, "segmentsUploaded")),
579 })
580}
581
582#[derive(Debug, Clone, PartialEq, Eq, Default)]
584pub struct FlushNode {
585 pub node_id: Option<String>,
586 pub complete: bool,
587 pub pending: bool,
588 pub timed_out: bool,
589 pub outbox_pending: u64,
590 pub memory_remaining: u64,
591 pub error: Option<String>,
592}
593
594#[derive(Debug, Clone, PartialEq, Eq, Default)]
595pub struct FlushReport {
596 pub complete: bool,
597 pub nodes: Vec<FlushNode>,
598}
599
600fn flush_node(f: &Fields<'_>) -> FlushNode {
601 FlushNode {
602 node_id: f.string("nodeId"),
603 complete: f.boolean("complete").unwrap_or(false),
604 pending: f.boolean("pending").unwrap_or(false),
605 timed_out: f.boolean("timedOut").unwrap_or(false),
606 outbox_pending: whole(f, "outboxPending").unwrap_or(0),
607 memory_remaining: whole(f, "memoryRemaining").unwrap_or(0),
608 error: f.string("error"),
609 }
610}
611
612pub fn parse_flush(text: &str) -> Result<FlushReport, String> {
614 let f = fields(text)?;
615 let complete = f.boolean("complete").unwrap_or(false);
616 let nodes = if f.raw("nodes").is_some() {
617 items(&f, "nodes")
618 .into_iter()
619 .filter_map(|raw| Fields::parse(raw).ok().map(|n| flush_node(&n)))
620 .collect()
621 } else {
622 vec![flush_node(&f)]
623 };
624 Ok(FlushReport { complete, nodes })
625}
626
627#[derive(Debug, Clone, PartialEq, Eq, Default)]
628pub struct StopReport {
629 pub stopped: Vec<String>,
630 pub stop_list: bool,
631 pub warnings: Vec<String>,
632 pub nodes: Vec<(String, bool)>,
633}
634
635pub fn parse_stop(text: &str) -> Result<StopReport, String> {
636 let f = fields(text)?;
637 let nodes = items(&f, "nodes")
638 .into_iter()
639 .filter_map(|raw| {
640 let n = Fields::parse(raw).ok()?;
641 Some((n.string("nodeId")?, n.boolean("drained").unwrap_or(false)))
642 })
643 .collect();
644 Ok(StopReport {
645 stopped: texts(&f, "stopped"),
646 stop_list: f.boolean("stopList").unwrap_or(false),
647 warnings: texts(&f, "warnings"),
648 nodes,
649 })
650}
651
652#[derive(Debug, Clone, PartialEq, Eq, Default)]
654pub struct Token {
655 pub id: String,
656 pub name: String,
657 pub permissions: Vec<String>,
658 pub prefixes: Vec<String>,
659 pub created_at: Option<String>,
660 pub expires_at: Option<String>,
661}
662
663fn token_of(f: &Fields<'_>) -> Option<Token> {
664 Some(Token {
665 id: f.string("id")?,
666 name: f.string("name").unwrap_or_default(),
667 permissions: texts(f, "permissions"),
668 prefixes: texts(f, "topicPrefixes"),
669 created_at: f.string("createdAt"),
670 expires_at: f.string("expiresAt"),
671 })
672}
673
674#[derive(Debug, Clone, PartialEq, Eq, Default)]
676pub struct TokenPage {
677 pub items: Vec<Token>,
678 pub next_cursor: Option<String>,
679}
680
681pub fn parse_tokens(text: &str) -> Result<TokenPage, String> {
682 let f = fields(text)?;
683 let list = items(&f, "items")
684 .into_iter()
685 .filter_map(|raw| token_of(&Fields::parse(raw).ok()?))
686 .collect();
687 Ok(TokenPage {
688 items: list,
689 next_cursor: f.string("nextCursor"),
690 })
691}
692
693pub fn parse_token_created(text: &str) -> Result<(Token, String), String> {
695 let f = fields(text)?;
696 let token = token_of(&f).ok_or_else(|| "the reply has no token id".to_owned())?;
697 let secret = f
698 .string("token")
699 .ok_or_else(|| "the reply has no secret".to_owned())?;
700 Ok((token, secret))
701}
702
703#[derive(Debug, Clone, PartialEq, Eq, Default)]
705pub struct Topic {
706 pub id: String,
707 pub name: String,
708 pub kind: String,
710 pub fifo: bool,
711 pub volatile: bool,
712 pub tags: Vec<(String, String)>,
713 pub prefixes: Vec<String>,
714 pub idle_timeout: Option<u64>,
715 pub max_message_bytes: Option<u64>,
717 pub ttl: Option<u64>,
718 pub max_deliveries: Option<u64>,
719 pub effective_max_message_bytes: Option<u64>,
721 pub effective_ttl: Option<u64>,
722 pub effective_max_deliveries: Option<u64>,
723 pub dead_letter: bool,
724 pub source: Option<String>,
726 pub created_at: Option<String>,
727}
728
729impl Topic {
730 pub fn is_distributor(&self) -> bool {
731 self.kind == "distributor"
732 }
733}
734
735fn pairs_of(f: &Fields<'_>, name: &str) -> Vec<(String, String)> {
736 f.raw(name)
737 .and_then(|raw| json::object_fields(raw).ok())
738 .unwrap_or_default()
739 .into_iter()
740 .filter_map(|(key, value)| Some((key, json::decode_string(value).ok()?)))
741 .collect()
742}
743
744fn topic_of(f: &Fields<'_>) -> Option<Topic> {
745 let effective = f.raw("effective").and_then(|r| Fields::parse(r).ok());
746 let own = |name: &str| effective.as_ref().and_then(|e| whole(e, name));
747 Some(Topic {
748 id: f.string("id").unwrap_or_default(),
749 name: f.string("name")?,
750 kind: f.string("type").unwrap_or_else(|| "queue".to_owned()),
751 fifo: f.boolean("fifo").unwrap_or(false),
752 volatile: f.boolean("volatile").unwrap_or(false),
753 tags: pairs_of(f, "tags"),
754 prefixes: texts(f, "prefixes"),
755 idle_timeout: whole(f, "idleTimeout"),
756 max_message_bytes: whole(f, "maxMessageBytes"),
757 ttl: whole(f, "ttl"),
758 max_deliveries: whole(f, "maxDeliveries"),
759 effective_max_message_bytes: own("maxMessageBytes"),
760 effective_ttl: own("ttl"),
761 effective_max_deliveries: own("maxDeliveries"),
762 dead_letter: f.boolean("deadLetter").unwrap_or(false),
763 source: f.string("source"),
764 created_at: f.string("createdAt"),
765 })
766}
767
768#[derive(Debug, Clone, PartialEq, Eq, Default)]
770pub struct TopicPage {
771 pub items: Vec<Topic>,
772 pub next_cursor: Option<String>,
773}
774
775pub fn parse_topics(text: &str) -> Result<TopicPage, String> {
776 let f = fields(text)?;
777 let list = items(&f, "items")
778 .into_iter()
779 .filter_map(|raw| topic_of(&Fields::parse(raw).ok()?))
780 .collect();
781 Ok(TopicPage {
782 items: list,
783 next_cursor: f.string("nextCursor"),
784 })
785}
786
787pub fn parse_topic(text: &str) -> Result<Topic, String> {
789 topic_of(&fields(text)?).ok_or_else(|| "the reply has no topic name".to_owned())
790}
791
792#[derive(Debug, Clone, PartialEq, Default)]
794pub struct TopicNumbers {
795 pub depth: Option<u64>,
796 pub publish_rate: Option<f64>,
797 pub consume_rate: Option<f64>,
798 pub oldest_age: Option<f64>,
800 pub worst: String,
802 pub severity: Severity,
803}
804
805#[derive(Debug, Clone, PartialEq, Default)]
807pub struct TopicStatsPage {
808 pub items: Vec<(String, TopicNumbers)>,
809 pub next_cursor: Option<String>,
810 pub partial: bool,
811 pub missing: Vec<Missing>,
812}
813
814pub fn parse_topic_stats(text: &str) -> Result<TopicStatsPage, String> {
815 let f = fields(text)?;
816 let list = items(&f, "items")
817 .into_iter()
818 .filter_map(|raw| {
819 let t = Fields::parse(raw).ok()?;
820 let severity = items(&t, "conditions")
821 .first()
822 .and_then(|c| Fields::parse(c).ok())
823 .and_then(|c| c.string("severity"))
824 .map_or(Severity::Ok, |s| Severity::parse(&s));
825 Some((
826 t.string("name")?,
827 TopicNumbers {
828 depth: whole(&t, "depth"),
829 publish_rate: num(&t, "publishRate"),
830 consume_rate: num(&t, "consumeRate"),
831 oldest_age: num(&t, "oldestAge"),
832 worst: t
833 .string("worstCondition")
834 .unwrap_or_else(|| "ok".to_owned()),
835 severity,
836 },
837 ))
838 })
839 .collect();
840 Ok(TopicStatsPage {
841 items: list,
842 next_cursor: f.string("nextCursor"),
843 partial: f.boolean("partial").unwrap_or(false),
844 missing: parse_missing(&f),
845 })
846}
847
848#[derive(Debug, Clone, PartialEq, Default)]
851pub struct TopicSample {
852 pub visible: u64,
853 pub invisible: u64,
854 pub publish_rate: f64,
856 pub consume_rate: f64,
857 pub ack_rate: f64,
858 pub oldest_age: Option<f64>,
860 pub groups: Vec<(String, f64)>,
862 pub partial: bool,
863}
864
865pub fn parse_topic_sample(text: &str) -> Result<TopicSample, String> {
866 let f = fields(text)?;
867 let groups = items(&f, "topGroups")
868 .into_iter()
869 .filter_map(|raw| {
870 let g = Fields::parse(raw).ok()?;
871 Some((g.string("groupId")?, num(&g, "oldestAge")?))
872 })
873 .collect();
874 Ok(TopicSample {
875 visible: whole(&f, "visibleMessages").unwrap_or(0),
876 invisible: whole(&f, "invisibleMessages").unwrap_or(0),
877 publish_rate: num(&f, "publishRate").unwrap_or(0.0),
878 consume_rate: num(&f, "consumeRate").unwrap_or(0.0),
879 ack_rate: num(&f, "ackRate").unwrap_or(0.0),
880 oldest_age: num(&f, "oldestAge"),
881 groups,
882 partial: f.boolean("partial").unwrap_or(false),
883 })
884}
885
886pub fn parse_received(text: &str) -> Result<Vec<taba_client::Message>, String> {
888 let f = fields(text)?;
889 items(&f, "messages")
890 .into_iter()
891 .map(|raw| taba_client::Message::parse(raw).map_err(|e| e.to_string()))
892 .collect()
893}
894
895#[derive(Debug, Clone, PartialEq, Eq, Default)]
898pub struct Published {
899 pub id: Option<String>,
900 pub duplicate: bool,
901 pub matched: Option<u64>,
902}
903
904pub fn parse_published(text: &str) -> Result<Published, String> {
905 let f = fields(text)?;
906 Ok(Published {
907 id: f.string("id"),
908 duplicate: f.boolean("duplicate").unwrap_or(false),
909 matched: whole(&f, "matched"),
910 })
911}
912
913pub fn parse_pruned(text: &str) -> Result<u64, String> {
915 let f = fields(text)?;
916 Ok(whole(&f, "removed").unwrap_or(0))
917}
918
919#[cfg(test)]
920mod tests;