Adding automatic IP address, some refactor and better texts. Upgrading version.
This commit is contained in:
45
src/cli.rs
45
src/cli.rs
@ -6,29 +6,50 @@ use structopt::StructOpt;
|
||||
about = "CLI to extract meteorology data from Open Meteo"
|
||||
)]
|
||||
pub enum Arguments {
|
||||
#[structopt(about = "Gets the current weather for a coordinate")]
|
||||
#[structopt(
|
||||
about = "Displays the current weather for your IP address automatically or for specific coordinates"
|
||||
)]
|
||||
CurrentWeather {
|
||||
#[structopt(short = "l", long, help = "Latitude as a decimal number")]
|
||||
latitude: f32,
|
||||
#[structopt(short = "L", long, help = "Longitude as a decimal number")]
|
||||
longitude: f32,
|
||||
#[structopt(
|
||||
short = "l",
|
||||
long,
|
||||
requires("longitude"),
|
||||
help = "Latitude as a decimal number"
|
||||
)]
|
||||
latitude: Option<f64>,
|
||||
#[structopt(
|
||||
short = "L",
|
||||
long,
|
||||
requires("latitude"),
|
||||
help = "Longitude as a decimal number"
|
||||
)]
|
||||
longitude: Option<f64>,
|
||||
|
||||
// Flags
|
||||
#[structopt(short = "a", long, help = "Prints all the parameters")]
|
||||
#[structopt(short = "a", long, help = "Displays all the information")]
|
||||
all: bool,
|
||||
#[structopt(short = "d", long, help = "Prints if it's day or night")]
|
||||
#[structopt(short = "d", long, help = "Displays if it's day or night")]
|
||||
is_day: bool,
|
||||
#[structopt(short = "t", long, help = "Prints the decimal temperature")]
|
||||
#[structopt(
|
||||
short = "t",
|
||||
long,
|
||||
help = "Displays the decimal temperature, in Celsius"
|
||||
)]
|
||||
temperature: bool,
|
||||
#[structopt(short = "w", long, help = "Prints the decimal wind speed")]
|
||||
#[structopt(short = "w", long, help = "Displays the decimal wind speed, in km/h")]
|
||||
windspeed: bool,
|
||||
#[structopt(short = "W", long, help = "Prints the wind direction angle")]
|
||||
#[structopt(short = "W", long, help = "Displays the wind direction, in degrees")]
|
||||
winddirection: bool,
|
||||
#[structopt(long = "coords", help = "Displays the latitude and the longitude")]
|
||||
include_coords: bool,
|
||||
#[structopt(long = "city", help = "Displays the city")]
|
||||
include_city: bool,
|
||||
|
||||
#[structopt(
|
||||
short = "c",
|
||||
long,
|
||||
help = "Cleans the output and only shows the values separated by commas.
|
||||
- ORDER: is_day, temperature, windspeed, winddirection"
|
||||
help = "Cleans the output and only displays the values separated by commas.
|
||||
- ORDER: latitude, longitude, city, is_day, temperature, windspeed, winddirection"
|
||||
)]
|
||||
clean: bool,
|
||||
},
|
||||
|
48
src/coords.rs
Normal file
48
src/coords.rs
Normal file
@ -0,0 +1,48 @@
|
||||
pub struct Coordinates {
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
}
|
||||
|
||||
impl Coordinates {
|
||||
pub fn new(latitude: f64, longitude: f64) -> Coordinates {
|
||||
Coordinates {
|
||||
latitude,
|
||||
longitude,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_correct_latitude(&self) -> bool {
|
||||
return self.latitude > -90.0 && self.latitude < 90.0;
|
||||
}
|
||||
|
||||
pub fn is_correct_longitude(&self) -> bool {
|
||||
return self.longitude > -180.0 && self.longitude < 180.0;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn valid_coords() {
|
||||
let coords = Coordinates {
|
||||
latitude: -20.0,
|
||||
longitude: 15.0,
|
||||
};
|
||||
|
||||
assert!(coords.is_correct_latitude());
|
||||
assert!(coords.is_correct_longitude());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_coords() {
|
||||
let coords = Coordinates {
|
||||
latitude: -95.0,
|
||||
longitude: 185.0,
|
||||
};
|
||||
|
||||
assert!(!coords.is_correct_latitude());
|
||||
assert!(!coords.is_correct_longitude());
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
use serde_json::Value;
|
||||
|
||||
pub struct CurrentWeather {
|
||||
current_weather: serde_json::Value,
|
||||
clean: bool,
|
||||
}
|
||||
|
||||
impl CurrentWeather {
|
||||
pub fn new(current_weather: serde_json::Value, clean: bool) -> CurrentWeather {
|
||||
CurrentWeather {
|
||||
current_weather,
|
||||
clean,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_simple_data(
|
||||
&self,
|
||||
key: &str,
|
||||
description: &str,
|
||||
end_text: Option<&str>,
|
||||
) -> String {
|
||||
let data = self.extract_raw(key);
|
||||
self.parse_simple_data(data, description, end_text)
|
||||
}
|
||||
|
||||
pub fn extract_raw(&self, key: &str) -> &Value {
|
||||
&self.current_weather[key]
|
||||
}
|
||||
|
||||
fn parse_simple_data(&self, data: &Value, descriptor: &str, end_text: Option<&str>) -> String {
|
||||
if self.clean {
|
||||
format!("{data}")
|
||||
} else {
|
||||
let end_text = end_text.unwrap_or("");
|
||||
format!("{descriptor}: {data}{end_text}")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_custom_data(&self, data: &Value, custom: &str) -> String {
|
||||
if self.clean {
|
||||
format!("{data}")
|
||||
} else {
|
||||
format!("{custom}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_data_clean() {
|
||||
let current_weather = serde_json::json!({
|
||||
"temperature": 25,
|
||||
});
|
||||
let weather = CurrentWeather::new(current_weather, true);
|
||||
assert_eq!(weather.extract_simple_data("temperature", "", None), "25");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_data_not_clean() {
|
||||
let current_weather = serde_json::json!({
|
||||
"temperature": 25,
|
||||
});
|
||||
let weather = CurrentWeather::new(current_weather, false);
|
||||
assert_eq!(
|
||||
weather.extract_simple_data("temperature", "Temperature", Some("°C")),
|
||||
"Temperature: 25°C"
|
||||
);
|
||||
}
|
||||
}
|
9
src/current_weather.rs
Normal file
9
src/current_weather.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CurrentWeather {
|
||||
pub is_day: i32,
|
||||
pub temperature: f64,
|
||||
pub windspeed: f64,
|
||||
pub winddirection: f64,
|
||||
}
|
100
src/current_weather_printer.rs
Normal file
100
src/current_weather_printer.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use crate::current_weather::CurrentWeather;
|
||||
|
||||
pub struct CurrentWeatherPrinter {
|
||||
current_weather: CurrentWeather,
|
||||
clean: bool,
|
||||
}
|
||||
|
||||
impl CurrentWeatherPrinter {
|
||||
pub fn new(current_weather: CurrentWeather, clean: bool) -> CurrentWeatherPrinter {
|
||||
CurrentWeatherPrinter {
|
||||
current_weather,
|
||||
clean,
|
||||
}
|
||||
}
|
||||
pub fn extract_day(&self) -> String {
|
||||
if self.current_weather.is_day == 1 {
|
||||
self.parse_custom_data(&self.current_weather.is_day.to_string(), "Day")
|
||||
} else {
|
||||
self.parse_custom_data(&self.current_weather.is_day.to_string(), "Night")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_temperature(&self) -> String {
|
||||
self.parse_simple_data(
|
||||
&self.current_weather.temperature.to_string(),
|
||||
"Temperature",
|
||||
Some("°C"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn extract_wind_speed(&self) -> String {
|
||||
self.parse_simple_data(
|
||||
&self.current_weather.windspeed.to_string(),
|
||||
"Wind speed",
|
||||
Some(" km/h"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn extract_wind_direction(&self) -> String {
|
||||
self.parse_simple_data(
|
||||
&self.current_weather.winddirection.to_string(),
|
||||
"Wind direction",
|
||||
Some("°"),
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_custom_data(&self, data: &str, custom: &str) -> String {
|
||||
if self.clean {
|
||||
format!("{data}")
|
||||
} else {
|
||||
format!("{custom}")
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_simple_data(&self, data: &str, descriptor: &str, end_text: Option<&str>) -> String {
|
||||
if self.clean {
|
||||
format!("{data}")
|
||||
} else {
|
||||
let end_text = end_text.unwrap_or("");
|
||||
format!("{descriptor}: {data}{end_text}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn clean_data() {
|
||||
let current_weather = CurrentWeather {
|
||||
is_day: 1,
|
||||
temperature: 12.5,
|
||||
windspeed: 7.0,
|
||||
winddirection: 90.0,
|
||||
};
|
||||
let printer = CurrentWeatherPrinter::new(current_weather, true);
|
||||
|
||||
assert_eq!(printer.extract_day(), "1");
|
||||
assert_eq!(printer.extract_temperature(), "12.5");
|
||||
assert_eq!(printer.extract_wind_speed(), "7");
|
||||
assert_eq!(printer.extract_wind_direction(), "90");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_data() {
|
||||
let current_weather = CurrentWeather {
|
||||
is_day: 0,
|
||||
temperature: 22.0,
|
||||
windspeed: 15.5,
|
||||
winddirection: 118.0,
|
||||
};
|
||||
let printer = CurrentWeatherPrinter::new(current_weather, false);
|
||||
|
||||
assert_eq!(printer.extract_day(), "Night");
|
||||
assert_eq!(printer.extract_temperature(), "Temperature: 22°C");
|
||||
assert_eq!(printer.extract_wind_speed(), "Wind speed: 15.5 km/h");
|
||||
assert_eq!(printer.extract_wind_direction(), "Wind direction: 118°");
|
||||
}
|
||||
}
|
4
src/ifconfig.rs
Normal file
4
src/ifconfig.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub fn extract_public_ip() -> Result<String, ureq::Error> {
|
||||
let body = ureq::get("https://ifconfig.me/ip").call()?.into_string()?;
|
||||
Ok(body)
|
||||
}
|
5
src/ip_api.rs
Normal file
5
src/ip_api.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub fn extract_coords_and_city(ip: &str) -> Result<serde_json::Value, ureq::Error> {
|
||||
let url = format!("http://ip-api.com/json/{}?fields=16592", ip);
|
||||
let body: serde_json::Value = ureq::get(&url).call()?.into_json()?;
|
||||
Ok(body)
|
||||
}
|
140
src/main.rs
140
src/main.rs
@ -1,11 +1,23 @@
|
||||
mod cli;
|
||||
mod current_weater;
|
||||
mod coords;
|
||||
|
||||
mod current_weather;
|
||||
mod current_weather_printer;
|
||||
mod ifconfig;
|
||||
mod ip_api;
|
||||
mod open_meteo;
|
||||
|
||||
use billboard::{Alignment, Billboard};
|
||||
use std::process::exit;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use cli::Arguments;
|
||||
use current_weater::CurrentWeather;
|
||||
use coords::Coordinates;
|
||||
|
||||
use current_weather_printer::CurrentWeatherPrinter;
|
||||
use ifconfig::extract_public_ip;
|
||||
use ip_api::extract_coords_and_city;
|
||||
use open_meteo::request_current_weather;
|
||||
|
||||
fn main() {
|
||||
let opts = Arguments::from_args();
|
||||
@ -18,60 +30,108 @@ fn main() {
|
||||
temperature,
|
||||
windspeed,
|
||||
winddirection,
|
||||
include_coords,
|
||||
include_city,
|
||||
clean,
|
||||
} => {
|
||||
if !all && !is_day && !temperature && !windspeed && !winddirection {
|
||||
if !all
|
||||
&& !is_day
|
||||
&& !temperature
|
||||
&& !windspeed
|
||||
&& !winddirection
|
||||
&& !include_coords
|
||||
&& !include_city
|
||||
{
|
||||
eprintln!("[ERROR] Provide at least one parameter to print, check --help");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let result = extract_weather(latitude, longitude);
|
||||
let mut weather = result.unwrap_or_else(|_| {
|
||||
eprintln!("[ERROR] Requesting current_weather for Latitude: {latitude}, Longitude: {longitude}");
|
||||
let coordinates: Coordinates;
|
||||
let city: Option<String>;
|
||||
|
||||
if latitude.is_some() && longitude.is_some() {
|
||||
let latitude = latitude.unwrap();
|
||||
let longitude = longitude.unwrap();
|
||||
coordinates = Coordinates::new(latitude, longitude);
|
||||
|
||||
if !coordinates.is_correct_latitude() {
|
||||
eprintln!("[ERROR] {} is not a valid latitude", coordinates.latitude);
|
||||
exit(1);
|
||||
} else if !coordinates.is_correct_longitude() {
|
||||
eprintln!("[ERROR] {} is not a valid longitude", coordinates.longitude);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
city = None;
|
||||
} else {
|
||||
let ip_result = extract_public_ip();
|
||||
let ip = ip_result.unwrap_or_else(|_| {
|
||||
eprintln!("[ERROR] Could not extract public IP from ifconfig");
|
||||
exit(1);
|
||||
});
|
||||
|
||||
let coords_result = extract_coords_and_city(&ip);
|
||||
let coords = coords_result.unwrap_or_else(|_| {
|
||||
eprintln!("[ERROR] Could not extract IP coordinates from ip_api");
|
||||
exit(1);
|
||||
});
|
||||
|
||||
let latitude = coords["lat"].as_f64().unwrap();
|
||||
let longitude = coords["lon"].as_f64().unwrap();
|
||||
coordinates = Coordinates::new(latitude, longitude);
|
||||
city = Some(coords["city"].as_str().unwrap().to_string());
|
||||
}
|
||||
|
||||
let result = request_current_weather(&coordinates);
|
||||
let current_weather = result.unwrap_or_else(|_| {
|
||||
eprintln!(
|
||||
"[ERROR] Requesting CurrentWeather for Lat: {}, Lon: {}",
|
||||
coordinates.latitude, coordinates.longitude
|
||||
);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
let mut string_vec: Vec<String> = Vec::new();
|
||||
|
||||
if !clean {
|
||||
string_vec.push(format!(
|
||||
"=== Current weather for Latitude: {latitude}, Longitude: {longitude} ==="
|
||||
));
|
||||
}
|
||||
let mut title_header: Vec<String> = Vec::new();
|
||||
title_header.push(String::from("=== Current weather"));
|
||||
|
||||
let current_weather_data = weather["current_weather"].take();
|
||||
let current_weather = CurrentWeather::new(current_weather_data, clean);
|
||||
if (all || include_city) && city.is_some() {
|
||||
title_header.push(String::from("for"));
|
||||
title_header.push(city.unwrap());
|
||||
}
|
||||
|
||||
if is_day || all {
|
||||
let is_day = current_weather.extract_raw("is_day");
|
||||
if is_day == "1" {
|
||||
string_vec.push(current_weather.parse_custom_data(is_day, "Day"));
|
||||
} else {
|
||||
string_vec.push(current_weather.parse_custom_data(is_day, "Night"));
|
||||
title_header.push(String::from("==="));
|
||||
string_vec.push(title_header.join(" "));
|
||||
} else {
|
||||
if all || include_coords {
|
||||
string_vec.push(coordinates.latitude.to_string());
|
||||
string_vec.push(coordinates.longitude.to_string());
|
||||
}
|
||||
if (all || include_city) && city.is_some() {
|
||||
string_vec.push(city.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
let current_weather_printer = CurrentWeatherPrinter::new(current_weather, clean);
|
||||
if is_day || all {
|
||||
string_vec.push(current_weather_printer.extract_day());
|
||||
}
|
||||
if temperature || all {
|
||||
string_vec.push(current_weather.extract_simple_data(
|
||||
"temperature",
|
||||
"Temperature",
|
||||
Some("°C"),
|
||||
));
|
||||
string_vec.push(current_weather_printer.extract_temperature());
|
||||
}
|
||||
|
||||
if windspeed || all {
|
||||
string_vec.push(current_weather.extract_simple_data(
|
||||
"windspeed",
|
||||
"Wind speed",
|
||||
Some(" km/h"),
|
||||
));
|
||||
string_vec.push(current_weather_printer.extract_wind_speed());
|
||||
}
|
||||
if winddirection || all {
|
||||
string_vec.push(current_weather_printer.extract_wind_direction());
|
||||
}
|
||||
|
||||
if winddirection || all {
|
||||
string_vec.push(current_weather.extract_simple_data(
|
||||
"winddirection",
|
||||
"Wind direction",
|
||||
Some("°"),
|
||||
if !clean && (all || include_coords) {
|
||||
string_vec.push(format!(
|
||||
"Latitude: {}, Longitude: {}",
|
||||
coordinates.latitude, coordinates.longitude
|
||||
));
|
||||
}
|
||||
|
||||
@ -81,14 +141,12 @@ fn main() {
|
||||
} else {
|
||||
string_vec.push(String::from("=== Weather data by Open-Meteo.com ==="));
|
||||
let final_string = string_vec.join("\n");
|
||||
println!("{final_string}");
|
||||
Billboard::builder()
|
||||
.text_alignment(Alignment::Left)
|
||||
.box_alignment(Alignment::Left)
|
||||
.build()
|
||||
.eprint(final_string);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_weather(latitude: f32, longitude: f32) -> Result<serde_json::Value, ureq::Error> {
|
||||
let url = format!("https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t_weather=true");
|
||||
let body: serde_json::Value = ureq::get(&url).call()?.into_json()?;
|
||||
Ok(body)
|
||||
}
|
||||
|
12
src/open_meteo.rs
Normal file
12
src/open_meteo.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use crate::{coords::Coordinates, current_weather::CurrentWeather};
|
||||
|
||||
pub fn request_current_weather(coords: &Coordinates) -> Result<CurrentWeather, ureq::Error> {
|
||||
let url = format!(
|
||||
"https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}¤t_weather=true",
|
||||
coords.latitude, coords.longitude
|
||||
);
|
||||
let mut body: serde_json::Value = ureq::get(&url).call()?.into_json()?;
|
||||
let current_weather = body["current_weather"].take();
|
||||
let current_weather: CurrentWeather = serde_json::from_value(current_weather).unwrap();
|
||||
Ok(current_weather)
|
||||
}
|
Reference in New Issue
Block a user