use bpaf::Bpaf;
use grabcli_core::error::Result;
use grabcli_core::location::Location;
use grabcli_core::output::Output;
use grabcli_core::places::{self, Match, SavedPlace};
use grabcli_core::{Context, Hydra, Request};
use serde_json::{Value, json};
#[derive(Debug, Clone, Bpaf)]
pub enum PlacesCmd {
#[bpaf(command("list"))]
List {
#[bpaf(long("json"), switch)]
json: bool,
#[bpaf(long("country"), argument("CODE"))]
country: Option<String>,
},
#[bpaf(command("use"))]
Use {
#[bpaf(long("json"), switch)]
json: bool,
#[bpaf(long("unit"), argument("TEXT"))]
unit: Option<String>,
#[bpaf(long("note"), argument("TEXT"))]
note: Option<String>,
#[bpaf(positional("PLACE"))]
place: String,
},
}
pub fn wants_json(cmd: &PlacesCmd) -> bool {
match cmd {
PlacesCmd::List { json, .. } | PlacesCmd::Use { json, .. } => *json,
}
}
pub async fn run(out: Output, cmd: PlacesCmd) -> Result<()> {
let mut ctx = Context::new(out, places::allowlist())?;
let all = fetch(&mut ctx).await?;
let current = Location::from_session(ctx.session()).ok();
match cmd {
PlacesCmd::List { country, .. } => {
list(out, &all, current.as_ref(), country.as_deref());
Ok(())
}
PlacesCmd::Use {
place, unit, note, ..
} => {
let p = places::find(&all, &place)?.clone();
let idx = all.iter().position(|q| q.poi_id == p.poi_id);
let same = current
.as_ref()
.filter(|c| idx.is_some() && places::current_place(&all, c).map(|(i, _)| i) == idx);
let unit = unit.or_else(|| same.and_then(|c| c.address_detail.clone()));
let note = note.or_else(|| same.and_then(|c| c.note_to_driver.clone()));
let cookie = p.to_location(unit.as_deref(), note.as_deref());
ctx.set_location(&cookie)?;
let loc = Location::from_session(ctx.session())?;
if out.is_json() {
out.json(&json!({
"place": place_json(&p, Some(Match::Current)),
"address": loc.address,
"address_detail": loc.address_detail,
"note_to_driver": loc.note_to_driver,
}));
} else {
out.line(format!("Delivering to {}", title(&p)));
out.line(format!(" {}", p.address));
if let Some(u) = &loc.address_detail {
out.line(format!(" Unit: {u}"));
}
if let Some(n) = &loc.note_to_driver {
out.line(format!(" Note: {n}"));
}
}
Ok(())
}
}
}
async fn fetch(ctx: &mut Context) -> Result<Vec<SavedPlace>> {
let v = ctx
.call(&Request::get(places::FAVORITES_URL, Hydra::Required))
.await?;
places::parse_places(&v)
}
fn title(p: &SavedPlace) -> String {
match p.display_label() {
Some(l) if !l.eq_ignore_ascii_case(&p.name) && !p.name.is_empty() => {
format!("{l} · {}", p.name)
}
Some(l) => l,
None => p.name.clone(),
}
}
fn place_json(p: &SavedPlace, m: Option<Match>) -> Value {
json!({
"poi_id": p.poi_id,
"label": p.display_label(),
"label_raw": p.label,
"name": p.name,
"address": p.address,
"latitude": p.location.latitude,
"longitude": p.location.longitude,
"city": p.city,
"city_id": p.city_id,
"country": p.country,
"country_code": p.country_code,
"current": m == Some(Match::Current),
"near_current": m == Some(Match::Near),
})
}
fn list(out: Output, all: &[SavedPlace], current: Option<&Location>, country: Option<&str>) {
let shown: Vec<(usize, &SavedPlace)> = all
.iter()
.enumerate()
.filter(|(_, p)| country.is_none_or(|c| p.country_code.eq_ignore_ascii_case(c)))
.collect();
let cur = current.and_then(|c| places::current_place(all, c));
let match_of = |i: usize| cur.filter(|(j, _)| *j == i).map(|(_, m)| m);
if out.is_json() {
let items: Vec<Value> = shown
.iter()
.map(|(i, p)| {
let mut v = place_json(p, match_of(*i));
v["index"] = json!(i + 1);
v
})
.collect();
out.json(&json!({
"places": items,
"current": current.map(|c| json!({
"poi_id": c.id,
"address": c.address,
"address_detail": c.address_detail,
"note_to_driver": c.note_to_driver,
})),
}));
return;
}
if shown.is_empty() {
out.line("No saved places.");
return;
}
for (i, p) in &shown {
let m = match_of(*i);
out.line(format!(
"{} {:>2} {} [{}]",
match m {
Some(Match::Current) => "*",
Some(Match::Near) => "~",
None => " ",
},
i + 1,
title(p),
p.country_code
));
out.line(format!(" {}", p.address));
if m.is_some()
&& let Some(c) = current
{
if let Some(u) = &c.address_detail {
out.line(format!(" Unit: {u}"));
}
if let Some(n) = &c.note_to_driver {
out.line(format!(" Note: {n}"));
}
}
}
}