#![deny(warnings, clippy::all)]
use chrono::NaiveDate;
use reqwest::{Client, Url};
use serde::Deserialize;
use std::error::Error;
use std::fmt::Display;
use std::io::Write;
#[derive(Deserialize, Debug, Clone)]
pub struct APODError {
pub code: u16,
pub msg: String,
pub service_version: String,
}
impl Display for APODError {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(fmt, "{} (code {})", self.msg, self.code)
}
}
pub fn apod_url(date: NaiveDate) -> Url {
Url::parse(&format!(
"https://apod.nasa.gov/apod/ap{}.html",
date.format("%y%m%d")
))
.unwrap()
}
#[derive(Deserialize, Debug, Clone)]
pub struct APOD {
pub date: NaiveDate,
pub title: String,
pub explanation: String,
#[serde(default)]
pub copyright: Option<String>,
pub media_type: String,
#[serde(default, with = "url_serde")]
pub hdurl: Option<Url>,
#[serde(with = "url_serde")]
pub url: Url,
pub service_version: String,
}
impl APOD {
pub fn download_url(&self) -> &Url {
self.hdurl.as_ref().unwrap_or(&self.url)
}
pub fn url(&self) -> Url {
apod_url(self.date)
}
}
#[derive(Debug)]
pub enum APODClientError {
APOD(APODError),
HTTP(reqwest::Error),
}
impl Display for APODClientError {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
use APODClientError::*;
match self {
APOD(error) => write!(fmt, "APOD error: {}", error),
HTTP(error) => write!(fmt, "HTTP error: {}", error),
}
}
}
impl Error for APODClientError {}
impl From<reqwest::Error> for APODClientError {
fn from(error: reqwest::Error) -> Self {
APODClientError::HTTP(error)
}
}
impl From<APODError> for APODClientError {
fn from(error: APODError) -> Self {
APODClientError::APOD(error)
}
}
type Result<T> = std::result::Result<T, APODClientError>;
trait APODResponse
where
Self: std::marker::Sized,
{
fn apod_response(self) -> Result<Self>;
}
impl APODResponse for reqwest::Response {
fn apod_response(mut self) -> Result<Self> {
if self.status().is_success() {
Ok(self)
} else {
Err(self.json::<APODError>()?.into())
}
}
}
#[derive(Debug)]
pub struct APODClient {
api_key: String,
client: Client,
}
impl APODClient {
pub fn new(api_key: String) -> APODClient {
let proxy = reqwest::Proxy::custom(|url| env_proxy::for_url(&url).to_url());
APODClient {
api_key,
client: Client::builder().proxy(proxy).build().unwrap(),
}
}
pub fn api_key(&self) -> &str {
&self.api_key
}
pub fn get(&self, date: NaiveDate) -> Result<APOD> {
let mut url = Url::parse("https://api.nasa.gov/planetary/apod").unwrap();
url.query_pairs_mut()
.append_pair("api_key", &self.api_key)
.append_pair("date", &date.format("%Y-%m-%d").to_string());
self.client
.get(url)
.send()?
.apod_response()?
.json()
.map_err(From::from)
}
pub fn copy_to<W: Write>(&self, apod: &APOD, sink: &mut W) -> Result<u64> {
self.client
.get(apod.download_url().clone())
.send()?
.apod_response()?
.copy_to(sink)
.map_err(From::from)
}
}
#[cfg(test)]
mod test {
use super::*;
use chrono::NaiveDate;
use pretty_assertions::assert_eq;
fn client() -> APODClient {
APODClient::new(
std::env::var("NASA_API_KEY").expect("NASA API KEY in $NASA_API_KEY missing"),
)
}
#[test]
fn apod_get_image() -> Result<()> {
let date = NaiveDate::from_ymd(2019, 4, 11);
let apod = client().get(date)?;
assert_eq!(apod.date, date);
assert_eq!(apod.media_type, "image");
assert_eq!(apod.title, "First Horizon-Scale Image of a Black Hole");
assert!(apod.copyright.is_none());
assert!(apod.hdurl.is_some());
Ok(())
}
#[test]
fn apod_get_video() -> Result<()> {
let date = NaiveDate::from_ymd(2019, 4, 10);
let apod = client().get(date)?;
assert_eq!(apod.date, date);
assert_eq!(apod.media_type, "video");
assert!(apod.copyright.is_none());
assert!(apod.hdurl.is_none());
Ok(())
}
#[test]
fn apod_get_image_with_copyright() -> Result<()> {
let date = NaiveDate::from_ymd(2019, 4, 8);
let apod = client().get(date)?;
assert_eq!(apod.date, date);
assert_eq!(apod.media_type, "image");
assert_eq!(apod.title, "AZURE Vapor Tracers over Norway");
assert_eq!(apod.copyright, Some("Yang Sutie".to_string()));
assert!(apod.hdurl.is_some());
Ok(())
}
#[test]
fn apod_get_image_does_not_exist() {
let error = match client().get(NaiveDate::from_ymd(1994, 8, 23)).unwrap_err() {
APODClientError::APOD(error) => error,
APODClientError::HTTP(error) => panic!("Unexpected HTTP error: {}", error),
};
assert_eq!(error.code, 400);
assert!(
error.msg.contains("Date must be between Jun 16, 1995"),
"Message: {}",
error.msg
);
}
#[test]
fn apod_copy_to_buffer() -> Result<()> {
let c = client();
let apod = c.get(NaiveDate::from_ymd(2019, 4, 11)).unwrap();
let mut buffer = Vec::new();
let no_bytes = c.copy_to(&apod, &mut buffer)?;
assert!(127_000 < no_bytes);
Ok(())
}
}