1use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
15pub struct Place {
16 pub lat: f64,
17 pub lon: f64,
18}
19
20#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
22pub struct Places {
23 #[serde(default)]
24 found: BTreeMap<String, Place>,
25 #[serde(default)]
27 unknown: Vec<String>,
28}
29
30pub fn normalize(address: &str) -> String {
32 address.lines().map(|l| l.split_whitespace().collect::<Vec<_>>().join(" ")).filter(|l| !l.is_empty()).collect::<Vec<_>>().join(", ")
33}
34
35impl Places {
36 pub fn default_path() -> PathBuf {
37 crate::config::cache_dir().join("places.toml")
38 }
39
40 pub fn load(path: &Path) -> Places {
41 std::fs::read_to_string(path).ok().and_then(|t| toml::from_str(&t).ok()).unwrap_or_default()
42 }
43
44 pub fn save(&self, path: &Path) -> Result<(), String> {
45 let fail = |e: std::io::Error| format!("{}: {e}", path.display());
46 if let Some(parent) = path.parent() {
47 std::fs::create_dir_all(parent).map_err(fail)?;
48 }
49 let temporary = path.with_extension("toml.new");
50 std::fs::write(&temporary, toml::to_string(self).map_err(|e| e.to_string())?).map_err(fail)?;
51 std::fs::rename(&temporary, path).map_err(fail)
52 }
53
54 pub fn get(&self, address: &str) -> Option<Place> {
56 self.found.get(&normalize(address)).copied()
57 }
58
59 pub fn asked(&self, address: &str) -> bool {
61 let key = normalize(address);
62 self.found.contains_key(&key) || self.unknown.contains(&key)
63 }
64
65 pub fn set(&mut self, address: &str, place: Option<Place>) {
66 let key = normalize(address);
67 match place {
68 Some(place) => {
69 self.unknown.retain(|k| *k != key);
70 self.found.insert(key, place);
71 }
72 None => {
73 if !self.unknown.contains(&key) {
74 self.unknown.push(key);
75 }
76 }
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn an_address_is_asked_once() {
87 let mut places = Places::default();
88 let address = "12 rue de l'Exemple\n 69000 Lyon\nFrance";
89 assert_eq!(normalize(address), "12 rue de l'Exemple, 69000 Lyon, France");
90 assert!(!places.asked(address));
91 places.set(address, Some(Place { lat: 45.76, lon: 4.83 }));
92 places.set("Nowhere 1", None);
93 assert!(places.asked("12 rue de l'Exemple, 69000 Lyon, France") && places.asked("Nowhere 1"));
94 let dir = std::env::temp_dir().join(format!("sioul-places-{}", std::process::id()));
95 let path = dir.join("places.toml");
96 places.save(&path).unwrap();
97 let read = Places::load(&path);
98 assert_eq!(read.get(address), Some(Place { lat: 45.76, lon: 4.83 }));
99 assert!(read.asked("Nowhere 1") && read.get("Nowhere 1").is_none());
100 let _ = std::fs::remove_dir_all(&dir);
101 }
102}