Skip to content

Commit

Permalink
feat(weather): add fallback (#83)
Browse files Browse the repository at this point in the history
If the weatherapi.com api-key is present _and_ the request to that api
fails, we will now failover to open-meteo.com api.

Resolves #76
  • Loading branch information
johnallen3d authored Sep 5, 2023
1 parent 7a0b899 commit 80450d9
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 49 deletions.
24 changes: 11 additions & 13 deletions src/conditions.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serde::Serialize;

use crate::{config::Config, weather::CurrentConditions};
use crate::{config::Config, weather::CurrentConditions, weather::Provider};

#[derive(Debug, Serialize)]
struct Output {
Expand All @@ -20,7 +20,7 @@ impl From<CurrentConditions> for Output {

Self {
temp: temp as i32,
icon: conditions.icon.unwrap_or_default(),
icon: conditions.icon,
}
}
}
Expand All @@ -42,25 +42,23 @@ impl Conditions {
/// - compose output structure
/// - convert output to JSON and return
pub fn fetch(&self) -> eyre::Result<String> {
let provider = match &self.config.weatherapi_token {
Some(token) => {
crate::weather::Provider::WeatherAPI(token.to_string())
}
None => crate::weather::Provider::OpenMeteo,
};
let mut providers = Vec::new();

if let Some(api_key) = &self.config.weatherapi_token {
providers.push(Provider::WeatherAPI(api_key.to_string()));
}

providers.push(Provider::OpenMeteo);

let location = self.config.get_location()?;

let mut conditions = crate::weather::CurrentConditions::get(
provider.clone(),
let conditions = CurrentConditions::get(
providers,
self.config.unit,
&location.latitude,
&location.longitude,
)?;

conditions
.set_icon(conditions.time_of_day.icon(provider, conditions.code));

let output = Output::from(conditions);

Ok(ureq::serde_json::to_string(&output)?)
Expand Down
7 changes: 1 addition & 6 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,9 @@ impl Config {
match &self.location {
Some(location) => Ok(location.clone()),
None => {
eprintln!(
"location not set, trying to infer via: {}",
location::from_ip::URL,
);

let inferred = location::get(None)?;

eprintln!("inferred location: {}", inferred.loc);
eprintln!("location not set, inferred postal code: {}", inferred.postal_code);

Ok(inferred)
}
Expand Down
58 changes: 37 additions & 21 deletions src/weather/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use crate::icons::TimeOfDay;
use std::fmt;

pub(crate) mod open_meteo;
pub(crate) mod weather_api;

#[derive(Debug)]
pub struct CurrentConditions {
pub code: i32,
pub temp_c: f32,
pub temp_f: f32,
pub time_of_day: TimeOfDay,
pub icon: Option<String>,
pub icon: String,
}

pub trait WeatherProvider {
Expand All @@ -19,34 +17,52 @@ pub trait WeatherProvider {

#[derive(Clone, Debug)]
pub enum Provider {
// contains api key
WeatherAPI(String),
OpenMeteo,
}

impl fmt::Display for Provider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Provider::WeatherAPI(_) => "WeatherAPI",
Provider::OpenMeteo => "OpenMeteo",
};
write!(f, "{}", name)
}
}

impl CurrentConditions {
pub fn get(
provider: Provider,
providers: Vec<Provider>,
unit: crate::Unit,
latitude: &str,
longitude: &str,
) -> eyre::Result<CurrentConditions> {
let client: Box<dyn WeatherProvider> = match provider {
Provider::WeatherAPI(key) => Box::new(weather_api::Client::new(
key,
latitude.to_string(),
longitude.to_string(),
)),
Provider::OpenMeteo => Box::new(open_meteo::Client::new(
unit,
latitude.to_string(),
longitude.to_string(),
)),
};
for provider in &providers {
let result = match provider {
Provider::WeatherAPI(key) => weather_api::Client::new(
key.to_string(),
latitude.to_string(),
longitude.to_string(),
)
.current(),
Provider::OpenMeteo => open_meteo::Client::new(
unit,
latitude.to_string(),
longitude.to_string(),
)
.current(),
};

client.current()
}
match result {
Ok(conditions) => return Ok(conditions),
Err(_) => {
eprintln!("error fetching weather from: {}", provider)
}
}
}

pub fn set_icon(&mut self, value: String) {
self.icon = Some(value);
Err(eyre::eyre!("no weather providers succeeded"))
}
}
10 changes: 6 additions & 4 deletions src/weather/open_meteo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ impl WeatherProvider for Client {

impl From<Response> for CurrentConditions {
fn from(result: Response) -> Self {
let icon = TimeOfDay::from(result.current_weather.is_day).icon(
super::Provider::OpenMeteo,
result.current_weather.weathercode,
);

Self {
code: result.current_weather.weathercode,
// TODO: this api only returns requested unit
temp_c: result.current_weather.temperature,
temp_f: result.current_weather.temperature,
time_of_day: TimeOfDay::from(result.current_weather.is_day),
icon: None,
icon,
}
}
}
11 changes: 6 additions & 5 deletions src/weather/weather_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use thiserror::Error;
use super::{CurrentConditions, WeatherProvider};
use crate::icons::TimeOfDay;

static WEATHERAPI_URL: &str = "http://api.weatherapi.com/v1/current.json";
static URL: &str = "http://api.weatherapi.com/v1/current.jso";

pub struct Client {
key: String,
Expand All @@ -23,7 +23,7 @@ impl Client {

impl WeatherProvider for Client {
fn current(&self) -> eyre::Result<CurrentConditions> {
let parsed = ureq::get(WEATHERAPI_URL)
let parsed = ureq::get(URL)
.query_pairs(self.query_pairs())
.call()?
.into_json::<WeatherAPIResult>()?;
Expand Down Expand Up @@ -84,12 +84,13 @@ struct WeatherAPIResultCondition {

impl From<WeatherAPIResult> for CurrentConditions {
fn from(result: WeatherAPIResult) -> Self {
let icon = TimeOfDay::from(result.current.is_day)
.icon(super::Provider::OpenMeteo, result.current.condition.code);

Self {
code: result.current.condition.code,
temp_c: result.current.temp_c,
temp_f: result.current.temp_f,
time_of_day: TimeOfDay::from(result.current.is_day),
icon: None,
icon,
}
}
}

0 comments on commit 80450d9

Please sign in to comment.