Skip to content

Commit eebf099

Browse files
committed
feat: add geocoding fallback
1 parent db30034 commit eebf099

File tree

3 files changed

+101
-46
lines changed

3 files changed

+101
-46
lines changed

src/main.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ mod modules;
33
use anyhow::Result;
44
use clap::Parser;
55

6-
use modules::{args::Cli, display::product::Product, location::Geolocation, params::Params, weather::Weather};
6+
use modules::{args::Cli, display::product::Product, location::GeoIpLocation, params::Params, weather::Weather};
77

88
#[tokio::main]
99
async fn main() -> Result<()> {
@@ -22,10 +22,10 @@ async fn main() -> Result<()> {
2222
}
2323

2424
pub async fn run(params: &Params) -> Result<Product> {
25-
let loc = Geolocation::search(&params.address, &params.language).await?;
25+
let loc = GeoIpLocation::search(&params.address, &params.language).await?;
2626
let (lat, lon) = (loc.lat.parse::<f64>().unwrap(), loc.lon.parse::<f64>().unwrap());
2727

28-
let address = loc.display_name.to_string();
28+
let address = loc.name.to_string();
2929
let weather = Weather::get(lat, lon, &params.units).await?;
3030

3131
Ok(Product { address, weather })

src/modules/location.rs

+95-40
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,116 @@
11
use anyhow::{anyhow, Result};
2-
use reqwest::{header::USER_AGENT, Client, Url};
2+
use reqwest::{Client, Url};
33

4-
use serde::{Deserialize, Serialize};
4+
use serde::Deserialize;
55

6-
// Geoip json
7-
#[derive(Serialize, Deserialize, Debug)]
8-
pub struct Geolocation {
6+
#[derive(Deserialize)]
7+
pub struct Address {
8+
pub name: String,
9+
pub lat: String,
10+
pub lon: String,
11+
}
12+
13+
#[derive(Deserialize)]
14+
pub struct GeoIpLocation {
915
pub latitude: f64,
1016
pub longitude: f64,
1117
pub city_name: String,
1218
pub country_code: String,
1319
}
1420

15-
// Open street map(OSM) json
16-
#[derive(Serialize, Deserialize, Debug, Clone)]
17-
pub struct Address {
18-
place_id: u64,
19-
licence: String,
20-
osm_type: String,
21-
osm_id: u64,
22-
boundingbox: Vec<String>,
23-
pub lat: String,
24-
pub lon: String,
25-
pub display_name: String,
26-
class: String,
27-
#[serde(rename(deserialize = "type"))]
28-
kind: String,
29-
importance: f64,
21+
#[derive(Deserialize)]
22+
struct OpenStreetMapGeoObj {
23+
// place_id: u64,
24+
// licence: String,
25+
// osm_type: String,
26+
// osm_id: u64,
27+
// boundingbox: Vec<String>,
28+
lat: String,
29+
lon: String,
30+
display_name: String,
31+
// place_rank: i32,
32+
// category: String,
33+
// #[serde(rename(deserialize = "type"))]
34+
// kind: String,
35+
// importance: f64,
36+
// icon: String,
37+
}
38+
39+
#[derive(Deserialize)]
40+
struct OpenMeteoResults {
41+
results: Vec<OpenMeteoGeoObj>,
3042
}
3143

32-
impl Geolocation {
33-
pub async fn get() -> Result<Geolocation> {
44+
#[derive(Deserialize)]
45+
struct OpenMeteoGeoObj {
46+
// id: i32,
47+
name: String,
48+
latitude: f64,
49+
longitude: f64,
50+
// elevation: f64,
51+
// timezone: String,
52+
// feature_code: String,
53+
// country_code: String,
54+
// country: String,
55+
// country_id: i32,
56+
// population: i32,
57+
// admin1: String,
58+
// admin2: String,
59+
// admin3: String,
60+
// admin4: String,
61+
// admin1_id: i32,
62+
// admin2_id: i32,
63+
// admin3_id: i32,
64+
// admin4_id: i32,
65+
// postcodes: Vec<String>,
66+
}
67+
68+
impl GeoIpLocation {
69+
pub async fn get() -> Result<GeoIpLocation> {
3470
let url = Url::parse("https://api.geoip.rs")?;
3571

36-
let res = reqwest::get(url).await?.json::<Geolocation>().await?;
72+
let res = reqwest::get(url).await?.json::<GeoIpLocation>().await?;
3773

3874
Ok(res)
3975
}
4076

41-
pub async fn search(address: &str, lang: &str) -> Result<Address> {
77+
async fn search_osm(client: &Client, address: &str, lang: &str) -> Result<Address> {
4278
let url = format!(
43-
"https://nominatim.openstreetmap.org/search?q={address}&accept-language={lang}&limit=1&format=json"
79+
"https://nominatim.openstreetmap.org/search?q={address}&accept-language={lang}&limit=1&format=jsonv2",
4480
);
81+
let results: Vec<OpenStreetMapGeoObj> = client.get(&url).send().await?.json().await?;
82+
let result = results.first().ok_or_else(|| anyhow!("Location request failed."))?;
4583

46-
let res = Client::new()
47-
.get(&url)
48-
.header(USER_AGENT, "wthrr-the-weathercrab")
49-
.send()
50-
.await?
51-
.json::<Vec<Address>>()
52-
.await?;
84+
Ok(Address {
85+
name: result.display_name.clone(),
86+
lon: result.lon.to_string(),
87+
lat: result.lat.to_string(),
88+
})
89+
}
5390

54-
if res.is_empty() {
55-
return Err(anyhow!("Location request failed."));
56-
}
91+
async fn search_open_meteo(client: &Client) -> Result<Address> {
92+
let url = "https://geocoding-api.open-meteo.com/v1/search?name=Berlin&language=fr";
93+
let results: OpenMeteoResults = client.get(url).send().await?.json().await?;
94+
let result = results
95+
.results
96+
.first()
97+
.ok_or_else(|| anyhow!("Location request failed."))?;
5798

58-
Ok(res[0].clone())
99+
Ok(Address {
100+
name: result.name.clone(),
101+
lon: result.longitude.to_string(),
102+
lat: result.latitude.to_string(),
103+
})
104+
}
105+
106+
pub async fn search(address: &str, lang: &str) -> Result<Address> {
107+
let client = Client::builder().user_agent("wthrr-the-weathercrab").build()?;
108+
let results = Self::search_osm(&client, address, lang).await;
109+
110+
match results {
111+
Ok(address) => Ok(address),
112+
Err(_) => Self::search_open_meteo(&client).await,
113+
}
59114
}
60115
}
61116

@@ -67,11 +122,11 @@ mod tests {
67122
async fn geolocation_response() -> Result<()> {
68123
let (address, lang_de, lang_pl) = ("berlin", "de", "pl");
69124

70-
let loc_de = Geolocation::search(address, lang_de).await?;
71-
let loc_pl = Geolocation::search(address, lang_pl).await?;
125+
let loc_de = GeoIpLocation::search(address, lang_de).await?;
126+
let loc_pl = GeoIpLocation::search(address, lang_pl).await?;
72127

73-
assert!(loc_de.display_name.contains("Deutschland"));
74-
assert!(loc_pl.display_name.contains("Niemcy"));
128+
assert!(loc_de.name.contains("Deutschland"));
129+
assert!(loc_pl.name.contains("Niemcy"));
75130

76131
Ok(())
77132
}

src/modules/params/address.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::{anyhow, Result};
22
use dialoguer::{theme::ColorfulTheme, Confirm};
33

4-
use crate::modules::{display::greeting, location::Geolocation, translation::translate};
4+
use crate::modules::{display::greeting, location::GeoIpLocation, translation::translate};
55

66
use super::Params;
77

@@ -25,7 +25,7 @@ impl Params {
2525
)
2626
.interact()?
2727
{
28-
let auto_loc = Geolocation::get().await?;
28+
let auto_loc = GeoIpLocation::get().await?;
2929
self.address = format!("{},{}", auto_loc.city_name, auto_loc.country_code);
3030
return Ok(());
3131
} else {
@@ -37,7 +37,7 @@ impl Params {
3737
// greeting with indentation to match overall style
3838
greeting::handle_greeting(self.gui.greeting, &self.language, true).await?;
3939
if arg_address == "auto" || (arg_address.is_empty() && self.address == "auto") {
40-
let auto_loc = Geolocation::get().await?;
40+
let auto_loc = GeoIpLocation::get().await?;
4141
self.address = format!("{},{}", auto_loc.city_name, auto_loc.country_code);
4242
} else if !arg_address.is_empty() {
4343
self.address = arg_address.to_string()

0 commit comments

Comments
 (0)