diff --git a/Cargo.lock b/Cargo.lock index 9c0d5ae..5b96436 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,6 +174,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "chrono-tz" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120" +dependencies = [ + "chrono", + "parse-zoneinfo", +] + [[package]] name = "cipher" version = "0.2.5" @@ -340,6 +350,7 @@ name = "fej" version = "0.0.1" dependencies = [ "chrono", + "chrono-tz", "regex", "reqwest", "rocket", @@ -1010,6 +1021,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "pear" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 6969693..3e94794 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" rocket = "0.4.7" serde = "1.0.124" chrono = "0.4.19" +chrono-tz = "0.5.3" regex = "1.4.5" [dependencies.reqwest] diff --git a/src/errors.rs b/src/errors.rs index cf78244..a34dba6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,4 @@ +// I can probably do this way easier using an external crate, I should do that use rocket::http::Status; pub enum FejError { @@ -20,3 +21,9 @@ impl From for FejError { FejError::FailedRequest } } + +impl From for FejError { + fn from(_: chrono::ParseError) -> FejError { + FejError::InvalidArgument + } +} diff --git a/src/ivago/controller/pickup_times.rs b/src/ivago/controller/pickup_times.rs index 0a9bd3d..955e840 100644 --- a/src/ivago/controller/pickup_times.rs +++ b/src/ivago/controller/pickup_times.rs @@ -1,5 +1,7 @@ use super::structs::{BasicDate, PickupTime, Street}; use crate::errors::FejError; +use chrono::DateTime; +use chrono_tz::Tz; use reqwest::blocking as reqwest; use std::collections::HashMap; use std::convert::{From, TryFrom}; @@ -8,10 +10,10 @@ const BASE_URL: &str = "https://www.ivago.be/nl/particulier/afval/ophaling"; const CAL_URL: &str = "https://www.ivago.be/nl/particulier/garbage/pick-up/pickups"; pub fn get_pickup_times( - street: Street, - number: u32, - start_date: BasicDate, - end_date: BasicDate, + street: &Street, + number: &u32, + start_date: &DateTime, + end_date: &DateTime, ) -> Result, FejError> { let client = reqwest::Client::builder().cookie_store(true).build()?; @@ -31,8 +33,8 @@ pub fn get_pickup_times( .query(&[ ("_format", "json"), ("type", ""), - ("start", &start_date.epoch().to_string()), - ("end", &end_date.epoch().to_string()), + ("start", &start_date.timestamp().to_string()), + ("end", &end_date.timestamp().to_string()), ]) .send()?; let data: Vec> = response.json()?; @@ -41,8 +43,9 @@ pub fn get_pickup_times( for map in data.iter() { output.push(PickupTime::new( - // TODO should I check here if the parsing worked? - BasicDate::try_from(map.get("date").unwrap().as_str()).unwrap(), + // TODO it's really not logical here that this would return an + // "InvalidArgument" error, it should just skip the item + BasicDate::try_from(map.get("date").unwrap().as_str())?, map.get("label").unwrap().to_string(), )) } diff --git a/src/ivago/controller/structs/basic_date.rs b/src/ivago/controller/structs/basic_date.rs index 421d7df..e7518fa 100644 --- a/src/ivago/controller/structs/basic_date.rs +++ b/src/ivago/controller/structs/basic_date.rs @@ -1,33 +1,16 @@ -use chrono::{FixedOffset, TimeZone}; -use regex::Regex; +use crate::errors::FejError; +use chrono::{DateTime, NaiveDate, TimeZone}; +use chrono_tz::Europe::Brussels; +use chrono_tz::Tz; use rocket::http::RawStr; use rocket::request::FromFormValue; use serde::ser::Serializer; use serde::Serialize; use std::convert::TryFrom; -/// Represents a very simple Timezoneless date. Considering the timezone will -/// always be CEST (aka Belgium's timezone), this is good enough. I use this -/// instead of a NaiveDate to avoid E0117. -pub struct BasicDate { - year: u32, - month: u8, - day: u8, -} - -impl BasicDate { - /// Return the seconds since epoch for this date - pub fn epoch(&self) -> i64 { - // Timezone of Brussels is UTC + 2 hours in the western hemisphere - FixedOffset::west(7_200) - .ymd(self.year as i32, self.month as u32, self.day as u32) - // For some reason, I couldn't get .timestamp() to work on a Date - // without a time component, even though the docs seemed to - // indicate this was possible - .and_hms(0, 0, 0) - .timestamp() - } -} +/// This class is a simple wrapper around chrono's DateTime. Its sole purpose +/// is to avoid error E0117. +pub struct BasicDate(pub DateTime); /// This allows us to use BasicDate as a query parameter in our routes. impl<'v> FromFormValue<'v> for BasicDate { @@ -41,29 +24,25 @@ impl<'v> FromFormValue<'v> for BasicDate { } } -/// We need this when deserializing BasicDate. -impl ToString for BasicDate { - fn to_string(&self) -> String { - format!("{}-{}-{}", self.year, self.month, self.day) +/// This is used to serialize BasicDate. +impl TryFrom<&str> for BasicDate { + type Error = FejError; + + fn try_from(s: &str) -> Result { + let naive_date = NaiveDate::parse_from_str(s, "%Y-%m-%d")?; + + Ok(BasicDate( + Brussels + .from_local_datetime(&naive_date.and_hms(0, 0, 0)) + .single() + .ok_or(FejError::InvalidArgument)?, + )) } } -/// This is used to serialize BasicDate. -impl TryFrom<&str> for BasicDate { - type Error = (); - - fn try_from(s: &str) -> Result { - let re = Regex::new(r"^(\d{4})-(\d{2})-(\d{2})$").unwrap(); - - match re.captures(s) { - None => Err(()), - Some(caps) => Ok(BasicDate { - // TODO change this to ? operator if possible - year: caps.get(1).unwrap().as_str().parse().unwrap(), - month: caps.get(2).unwrap().as_str().parse().unwrap(), - day: caps.get(3).unwrap().as_str().parse().unwrap(), - }), - } +impl From<&BasicDate> for String { + fn from(date: &BasicDate) -> String { + format!("{}", date.0.format("%Y-%m-%d")) } } @@ -72,6 +51,6 @@ impl Serialize for BasicDate { where S: Serializer, { - serializer.serialize_str(&self.to_string()) + serializer.serialize_str(&String::from(self)) } } diff --git a/src/ivago/controller/structs/street.rs b/src/ivago/controller/structs/street.rs index 35f7e16..1ca64ec 100644 --- a/src/ivago/controller/structs/street.rs +++ b/src/ivago/controller/structs/street.rs @@ -10,8 +10,8 @@ pub struct Street { pub city: String, } -impl From for String { - fn from(street: Street) -> String { +impl From<&Street> for String { + fn from(street: &Street) -> String { format!("{} ({})", street.name, street.city) } } diff --git a/src/ivago/mod.rs b/src/ivago/mod.rs index 78b1fc2..4a03e48 100644 --- a/src/ivago/mod.rs +++ b/src/ivago/mod.rs @@ -24,6 +24,9 @@ pub fn route_get_pickup_times( end_date: BasicDate, ) -> Result>, Status> { Ok(Json(get_pickup_times( - street, number, start_date, end_date, + &street, + &number, + &start_date.0, + &end_date.0, )?)) } diff --git a/src/main.rs b/src/main.rs index 9452f9f..485f370 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,8 @@ #[macro_use] extern crate rocket; +extern crate chrono_tz; + // Route modules mod catchers; mod errors;