1use serde::Deserialize;
6
7use crate::error::{Error, ErrorKind, Result};
8use crate::session::Session;
9
10#[derive(Debug, Clone, Deserialize)]
11pub struct Location {
12 #[serde(default)]
13 pub id: String,
14 pub latitude: f64,
15 pub longitude: f64,
16 #[serde(default)]
17 pub address: String,
18 #[serde(default, rename = "countryCode")]
19 pub country_code: String,
20 #[serde(default, rename = "addressDetail", deserialize_with = "non_empty")]
22 pub address_detail: Option<String>,
23 #[serde(default, rename = "noteToDriver", deserialize_with = "non_empty")]
25 pub note_to_driver: Option<String>,
26}
27
28fn non_empty<'de, D: serde::Deserializer<'de>>(
30 d: D,
31) -> std::result::Result<Option<String>, D::Error> {
32 let v: Option<String> = Option::deserialize(d)?;
33 Ok(v.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()))
34}
35
36impl Location {
37 pub fn from_session(session: &Session) -> Result<Self> {
38 let raw = session.cookie("location").ok_or_else(|| {
39 Error::new(
40 ErrorKind::AddressMissing,
41 "no saved delivery address (location cookie) in the session; copy it from the \
42 browser with `grab auth import --address-only` (set an address on food.grab.com \
43 there first if it has none)",
44 )
45 })?;
46 let decoded =
47 urlencoding::decode(raw).map_err(|e| Error::parse(format!("location cookie: {e}")))?;
48 serde_json::from_str(&decoded)
49 .map_err(|e| Error::parse(format!("location cookie JSON: {e}")))
50 }
51
52 pub fn delivery_notes(&self, extra: Option<&str>) -> String {
54 let extra = extra.map(str::trim).filter(|s| !s.is_empty());
55 match (self.note_to_driver.as_deref(), extra) {
56 (Some(a), Some(b)) => format!("{a}; {b}"),
57 (Some(a), None) => a.to_string(),
58 (None, Some(b)) => b.to_string(),
59 (None, None) => String::new(),
60 }
61 }
62
63 pub fn latlng(&self) -> String {
65 format!("{},{}", self.latitude, self.longitude)
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::session::Cookie;
73
74 #[test]
75 fn parses_url_encoded_location() -> Result<()> {
76 let json = r#"{"id":"POI-EXAMPLE","latitude":1.23,"longitude":4.56,"address":"1 Example St","countryCode":"PH"}"#;
78 let encoded = urlencoding::encode(json).into_owned();
79 let s = Session {
80 cookies: vec![Cookie {
81 name: "location".into(),
82 value: encoded,
83 domain: "food.grab.com".into(),
84 path: "/".into(),
85 expires: -1.0,
86 }],
87 ..Default::default()
88 };
89 let loc = Location::from_session(&s)?;
90 assert_eq!(loc.id, "POI-EXAMPLE");
91 assert_eq!(loc.latlng(), "1.23,4.56");
92 assert_eq!(loc.address, "1 Example St");
93 assert_eq!(loc.address_detail, None);
94 assert_eq!(loc.note_to_driver, None);
95 Ok(())
96 }
97
98 #[test]
99 fn parses_address_detail_and_driver_note() -> Result<()> {
100 let json = r#"{"id":"P","latitude":1.0,"longitude":2.0,"address":"1 Example St","addressDetail":"Unit 1A, Example Tower","noteToDriver":"Leave at lobby"}"#;
101 let loc: Location = serde_json::from_str(json).map_err(|e| Error::parse(e.to_string()))?;
102 assert_eq!(
103 loc.address_detail.as_deref(),
104 Some("Unit 1A, Example Tower")
105 );
106 assert_eq!(loc.note_to_driver.as_deref(), Some("Leave at lobby"));
107 Ok(())
108 }
109
110 #[test]
111 fn null_or_blank_detail_is_none() -> Result<()> {
112 let json = r#"{"latitude":1.0,"longitude":2.0,"addressDetail":null,"noteToDriver":" "}"#;
113 let loc: Location = serde_json::from_str(json).map_err(|e| Error::parse(e.to_string()))?;
114 assert_eq!(loc.address_detail, None);
115 assert_eq!(loc.note_to_driver, None);
116 Ok(())
117 }
118
119 #[test]
120 fn delivery_notes_append() -> Result<()> {
121 let mut loc: Location = serde_json::from_str(r#"{"latitude":1.0,"longitude":2.0}"#)
122 .map_err(|e| Error::parse(e.to_string()))?;
123 assert_eq!(loc.delivery_notes(None), "");
124 assert_eq!(loc.delivery_notes(Some("Ring bell")), "Ring bell");
125 loc.note_to_driver = Some("Leave at lobby".into());
126 assert_eq!(loc.delivery_notes(None), "Leave at lobby");
127 assert_eq!(loc.delivery_notes(Some(" ")), "Leave at lobby");
128 assert_eq!(
129 loc.delivery_notes(Some("Ring bell")),
130 "Leave at lobby; Ring bell"
131 );
132 Ok(())
133 }
134
135 #[test]
136 fn missing_location_errors() {
137 let e = Location::from_session(&Session::default()).expect_err("should error");
138 assert_eq!(e.kind, ErrorKind::AddressMissing);
139 }
140}