use super::search::Street; use chrono::{FixedOffset, TimeZone}; use regex::Regex; use reqwest::blocking as reqwest; use rocket::http::RawStr; use rocket::request::FromFormValue; use serde::ser::{SerializeStruct, Serializer}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::convert::{From, TryFrom}; use std::error::Error; 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"; /// Represents a very simple Timezoneless date. Considering the timezone will /// always be CEST (aka Belgium's timezone), this is good enough. pub struct BasicDate { year: u32, month: u8, day: u8, } impl<'v> FromFormValue<'v> for BasicDate { type Error = &'v RawStr; fn from_form_value(form_value: &'v RawStr) -> Result { // Beautiful how this exact example is in the docs match BasicDate::try_from(form_value.as_str()) { Err(_) => Err(form_value), // Here, we can assume these parses will work, because the regex // didn't fail Ok(date) => Ok(date), } } } impl ToString for BasicDate { fn to_string(&self) -> String { format!("{}-{}-{}", self.year, self.month, self.day) } } 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(()), // Here, we can assume these parses will work, because the regex // didn't fail Some(caps) => Ok(BasicDate { 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 Serialize for BasicDate { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_string()) } } impl From for i64 { fn from(date: BasicDate) -> i64 { // Timezone of Brussels is UTC + 2 hours in the western hemisphere FixedOffset::west(7_200) .ymd(date.year as i32, date.month as u32, date.day as u32) .and_hms(0, 0, 0) .timestamp() } } impl Serialize for PickupTime { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut s = serializer.serialize_struct("PickupTime", 4)?; s.serialize_field("date", &self.date)?; s.serialize_field("label", &self.label)?; s.serialize_field("classes", &self.classes)?; s.serialize_field("url", &self.url)?; s.end() } } /// Represents a pickup time instance. All fields are a direct map of the /// original API pub struct PickupTime { date: BasicDate, label: String, classes: Vec, url: String, } pub fn get_pickup_times( street: Street, number: u32, start_date: BasicDate, end_date: BasicDate, ) -> Result, Box> { let client = reqwest::Client::builder().cookie_store(true).build()?; // This populates the cookies with the necessary values client .post(BASE_URL) .form(&[ ("garbage_type", ""), ("ivago_street", &String::from(street)), ("number", &number.to_string()), ("form_id", "garbage_address_form"), ]) .send()?; let response = client .get(CAL_URL) .query(&[ ("_format", "json"), ("type", ""), ("start", &i64::from(start_date).to_string()), ("end", &i64::from(end_date).to_string()), ]) .send()?; let data: Vec> = response.json()?; let mut output: Vec = Vec::new(); for map in data.iter() { output.push(PickupTime { // TODO should I check here if the parsing worked? date: BasicDate::try_from(map.get("date").unwrap().as_str()).unwrap(), label: map.get("label").unwrap().to_string(), classes: map .get("classes") .unwrap() .split_whitespace() .map(|x| String::from(x)) .collect(), url: map.get("url").unwrap().to_string(), }) } Ok(output) }