diff --git a/Cargo.lock b/Cargo.lock index e248c93..9c0d5ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + [[package]] name = "atty" version = "0.2.14" @@ -331,6 +340,7 @@ name = "fej" version = "0.0.1" dependencies = [ "chrono", + "regex", "reqwest", "rocket", "rocket_contrib", @@ -1190,6 +1200,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" + [[package]] name = "remove_dir_all" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index 53b452c..6969693 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" +regex = "1.4.5" [dependencies.reqwest] version = "0.11.2" diff --git a/Makefile b/Makefile index 2f9e53d..a1baf59 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ push: # Run run: - @ cargo run + @ RUST_BACKTRACE=1 cargo run .PHONY: run @@ -36,6 +36,10 @@ format: @ cargo fmt .PHONY: format +lint: + @ cargo fmt -- --check +.PHONY: lint + # Documentation docs: diff --git a/src/ivago/controller/mod.rs b/src/ivago/controller/mod.rs index 6ff9dff..8e3cb62 100644 --- a/src/ivago/controller/mod.rs +++ b/src/ivago/controller/mod.rs @@ -1,6 +1,6 @@ mod pickup_times; mod search; -pub use pickup_times::{get_pickup_times, PickupTime}; +pub use pickup_times::{get_pickup_times, BasicDate, PickupTime}; pub use search::{search_streets, Street}; ///// Return the known pickup times for the given street and/or city diff --git a/src/ivago/controller/pickup_times.rs b/src/ivago/controller/pickup_times.rs index 1f037a2..e4ce656 100644 --- a/src/ivago/controller/pickup_times.rs +++ b/src/ivago/controller/pickup_times.rs @@ -1,36 +1,83 @@ use super::search::Street; -use chrono::NaiveDate; +use regex::Regex; use rocket::http::RawStr; use rocket::request::FromFormValue; +use serde::ser::{Serialize, SerializeStruct, Serializer}; use std::error::Error; const BASE_URL: &str = "https://www.ivago.be/nl/particulier/afval/ophaling"; +/// 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 + let re = Regex::new(r"^(\d{4})-(\d{2})-(\d{2})$").unwrap(); + match re.captures(form_value) { + None => Err(form_value), + // 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 ToString for BasicDate { + fn to_string(&self) -> String { + format!("{}-{}-{}", self.year, self.month, self.day) + } +} + +impl Serialize for BasicDate { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +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: NaiveDate, + date: BasicDate, label: String, classes: Vec, url: String, } -impl<'v> FromFormValue<'v> for NaiveDate { - type Error = &'v RawStr; - - fn from_form_value(form_value: &'v RawStr) -> Result { - match NaiveDate::parse_from_str(form_value, "%Y-%m-%d") { - Ok(date) => Ok(date), - Err(_) => Err(form_value), - } - } -} - pub fn get_pickup_times( street: Street, - number: u64, - start_date: NaiveDate, - end_date: NaiveDate, + number: u32, + start_date: BasicDate, + end_date: BasicDate, ) -> Result, Box> { Ok(Vec::new()) } diff --git a/src/ivago/controller/search.rs b/src/ivago/controller/search.rs index 2df30b9..be81254 100644 --- a/src/ivago/controller/search.rs +++ b/src/ivago/controller/search.rs @@ -1,4 +1,7 @@ +use regex::Regex; use reqwest::blocking as reqwest; +use rocket::http::RawStr; +use rocket::request::FromFormValue; use serde::ser::{Serialize, SerializeStruct, Serializer}; use std::collections::HashMap; use std::convert::TryFrom; @@ -40,6 +43,23 @@ impl TryFrom for Street { } } +impl<'v> FromFormValue<'v> for Street { + type Error = &'v RawStr; + + fn from_form_value(form_value: &'v RawStr) -> Result { + // This regex is pretty loose tbh, but not sure how I can make it more + // strict right now + let re = Regex::new(r"^(.+) \((.+)\)$").unwrap(); + match re.captures(&form_value.url_decode_lossy()) { + None => Err(form_value), + Some(caps) => Ok(Street { + name: String::from(caps.get(1).unwrap().as_str()), + city: String::from(caps.get(2).unwrap().as_str()), + }), + } + } +} + /// Represents a street pub struct Street { pub name: String, diff --git a/src/ivago/mod.rs b/src/ivago/mod.rs index 386bf36..33ab64e 100644 --- a/src/ivago/mod.rs +++ b/src/ivago/mod.rs @@ -2,12 +2,11 @@ mod controller; #[cfg(test)] mod tests; -use chrono::NaiveDate; use rocket::http::Status; use rocket_contrib::json::Json; pub fn routes() -> Vec { - routes![route_search_streets,] + routes![route_search_streets, route_get_pickup_times] } // URL: https://www.ivago.be/nl/particulier/autocomplete/garbage/streets?q=Lange @@ -15,7 +14,7 @@ pub fn routes() -> Vec { pub fn route_search_streets(street: String) -> Result>, Status> { match controller::search_streets(&street) { Ok(streets) => Ok(Json(streets)), - Err(err) => Err(Status::InternalServerError), + Err(_) => Err(Status::InternalServerError), } } @@ -23,8 +22,12 @@ pub fn route_search_streets(street: String) -> Result Result>, Status> { - Err(Status::InternalServerError) + match controller::get_pickup_times(street, number, start_date, end_date) { + // TODO provide more meaningful status codes here + Err(_) => Err(Status::InternalServerError), + Ok(times) => Ok(Json(times)), + } }