Skip to main content

grabcli_service/
text.rs

1//! The client's answers as plain text (Markdown-ish: ids in backticks), for
2//! a chat reply or a language model.
3
4use super::api::{Cart, Menu, Merchant, Quote, Tracked, money};
5
6fn opt<T: core::fmt::Display>(label: &str, v: Option<T>) -> String {
7    v.map_or_else(String::new, |v| format!(" · {label}{v}"))
8}
9
10/// A search's merchants, one per line.
11pub fn merchants(ms: &[Merchant]) -> String {
12    if ms.is_empty() {
13        return "No restaurants found.".into();
14    }
15    ms.iter()
16        .map(|m| {
17            let mut s = format!("- {} (`{}`)", m.name, m.id);
18            if !m.cuisine.is_empty() {
19                s.push_str(&format!(" · {}", m.cuisine));
20            }
21            s.push_str(&opt("ETA ", m.eta_min.map(|e| format!("{e} min"))));
22            s.push_str(&opt("", m.distance_km.map(|d| format!("{d:.1} km"))));
23            s.push_str(&opt("★", m.rating));
24            if !m.promo.is_empty() {
25                s.push_str(&format!(" · {}", m.promo));
26            }
27            if !m.open {
28                s.push_str(" · closed");
29            }
30            s
31        })
32        .collect::<Vec<_>>()
33        .join("\n")
34}
35
36/// A menu, one item per line; `query`: only the items whose name, category or description has it.
37pub fn menu(m: &Menu, query: Option<&str>) -> String {
38    let q = query.map(str::to_lowercase).filter(|q| !q.is_empty());
39    let mut out = format!("{} (`{}`)\n", m.merchant.name, m.merchant.id);
40    let mut n = 0;
41    for i in &m.items {
42        if let Some(q) = &q
43            && ![&i.name, &i.category, &i.description]
44                .iter()
45                .any(|f| f.to_lowercase().contains(q))
46        {
47            continue;
48        }
49        n += 1;
50        out.push_str(&format!(
51            "- {} (`{}`) {}",
52            i.name,
53            i.id,
54            money(i.price, &m.currency)
55        ));
56        if !i.category.is_empty() {
57            out.push_str(&format!(" · {}", i.category));
58        }
59        if !i.available {
60            out.push_str(" · unavailable");
61        }
62        out.push('\n');
63    }
64    if n == 0 {
65        out.push_str("No matching items.\n");
66    }
67    out.trim_end().to_owned()
68}
69
70/// The cart, with its subtotal.
71pub fn cart(c: &Cart) -> String {
72    if c.lines.is_empty() {
73        return "The cart is empty.".into();
74    }
75    let mut out = format!("Cart at {} (`{}`)\n", c.merchant_name, c.merchant_id);
76    for l in &c.lines {
77        out.push_str(&format!(
78            "- {}× {} (`{}`) {}",
79            l.quantity,
80            l.name,
81            l.item_id,
82            money(l.amount, &c.currency)
83        ));
84        if !l.note.is_empty() {
85            out.push_str(&format!(" ({})", l.note));
86        }
87        out.push('\n');
88    }
89    out.push_str(&format!("Subtotal {}", money(c.subtotal, &c.currency)));
90    out
91}
92
93/// A quote: lines, fees, discount, total, payment and where it goes (a
94/// label). The caller adds how to order it (its own command or tool).
95pub fn quote(q: &Quote) -> String {
96    let c = q.currency.as_str();
97    let mut out = format!("Quote `{}` at {}\n", q.quote_id, q.merchant_name);
98    for l in &q.lines {
99        out.push_str(&format!(
100            "- {}× {} {}\n",
101            l.quantity,
102            l.name,
103            money(l.amount, c)
104        ));
105    }
106    out.push_str(&format!("Subtotal {}\n", money(q.subtotal, c)));
107    for f in &q.fees {
108        out.push_str(&format!("{} {}\n", f.label, money(f.amount, c)));
109    }
110    if q.discount > 0 {
111        out.push_str(&format!(
112            "Discount ({}) -{}\n",
113            q.voucher,
114            money(q.discount, c)
115        ));
116    }
117    out.push_str(&format!("Total {}\n", money(q.total, c)));
118    if let Some(e) = q.eta_min {
119        out.push_str(&format!("ETA about {e} min\n"));
120    }
121    out.push_str(&format!(
122        "Pay with {} · deliver to {}",
123        q.payment_method, q.address_label
124    ));
125    out
126}
127
128/// Tracked orders, one per line.
129pub fn tracked(ts: &[Tracked]) -> String {
130    if ts.is_empty() {
131        return "No active orders.".into();
132    }
133    ts.iter()
134        .map(|t| {
135            let mut s = format!("- `{}` {}: {}", t.order_id, t.merchant_name, t.status);
136            s.push_str(&opt("ETA ", t.eta_min.map(|e| format!("{e} min"))));
137            if !t.driver.is_empty() {
138                s.push_str(&format!(" · driver {}", t.driver));
139            }
140            s
141        })
142        .collect::<Vec<_>>()
143        .join("\n")
144}