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..0ebc525 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,26 @@ version = "0.0.1" authors = ["Jef Roosens "] edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "fej_lib" +src = "src/lib.rs" +test = true +bench = true +doc = true +doctest = true +[[bin]] +name = "fej" +src = "src/main.rs" +test = false +bench = false +doc = false + [dependencies] 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/Dockerfile b/Dockerfile index 4a3ed8b..457e805 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,11 +13,13 @@ RUN apk update && apk add --no-cache openssl-dev build-base curl && \ COPY Cargo.toml Cargo.lock ./ COPY src/ ./src/ -# Finally, build the project +# Run the tests, don't want no broken docker image +# And then finally, build the project # Thank the lords that this article exists # https://users.rust-lang.org/t/sigsegv-with-program-linked-against-openssl-in-an-alpine-container/52172 # TODO add what these flags do & why they work -RUN RUSTFLAGS="-C target-feature=-crt-static" cargo build --release +RUN RUSTFLAGS="-C target-feature=-crt-static" cargo test && \ + RUSTFLAGS="-C target-feature=-crt-static" cargo build --release --bin fej # Now, we create the actual image diff --git a/Makefile b/Makefile index a1baf59..ab50282 100644 --- a/Makefile +++ b/Makefile @@ -23,13 +23,13 @@ push: # Run run: - @ RUST_BACKTRACE=1 cargo run + @ RUST_BACKTRACE=1 cargo run --bin fej .PHONY: run # Testing test: - @ cargo test + @ cargo test --no-fail-fast .PHONY: test format: diff --git a/benches/ivago.rs b/benches/ivago.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/benches/ivago.rs @@ -0,0 +1 @@ + diff --git a/src/errors.rs b/src/errors.rs index cf78244..dfb137f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,7 @@ +// I can probably do this way easier using an external crate, I should do that use rocket::http::Status; +#[derive(Debug, PartialEq)] pub enum FejError { InvalidArgument, FailedRequest, @@ -20,3 +22,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..d2721f9 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,20 +33,22 @@ 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()?; let mut output: Vec = Vec::new(); - 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(), - map.get("label").unwrap().to_string(), - )) + for map in data + .iter() + .filter(|m| m.contains_key("date") && m.contains_key("label")) + { + // Because we filtered the maps in the loop, we can safely us unwrap here + if let Ok(date) = BasicDate::try_from(map.get("date").unwrap().as_str()) { + output.push(PickupTime::new(date, map.get("label").unwrap().to_string())) + } } Ok(output) diff --git a/src/ivago/controller/search.rs b/src/ivago/controller/search.rs index 8c84113..e66652d 100644 --- a/src/ivago/controller/search.rs +++ b/src/ivago/controller/search.rs @@ -13,23 +13,17 @@ const SEARCH_URL: &str = "https://www.ivago.be/nl/particulier/autocomplete/garba /// /// * `street` - name of the street /// * `city` - city the street is in -// TODO find out how to do this async pub fn search_streets(street_name: &str) -> Result, FejError> { let client = reqwest::Client::new(); let response = client.get(SEARCH_URL).query(&[("q", street_name)]).send()?; let data: Vec> = response.json()?; - let mut output: Vec = Vec::new(); - - // We iterate over every item and extract the needed data - for map in data.iter() { - if let Some(value) = map.get("value") { - match Street::try_from(value.as_str()) { - Ok(street) => output.push(street), - Err(_) => continue, - } - } - } - - Ok(output) + // This is pretty cool, filter_map first does get() on all the maps, and + // then filters out any None values + // Then, we do the same thing for streets + Ok(data + .iter() + .filter_map(|m| m.get("value")) + .filter_map(|v| Street::try_from(v.as_str()).ok()) + .collect()) } diff --git a/src/ivago/controller/structs/basic_date.rs b/src/ivago/controller/structs/basic_date.rs index 421d7df..97ef3e4 100644 --- a/src/ivago/controller/structs/basic_date.rs +++ b/src/ivago/controller/structs/basic_date.rs @@ -1,33 +1,17 @@ -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. +#[derive(Debug, PartialEq)] +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 +25,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 +52,18 @@ impl Serialize for BasicDate { where S: Serializer, { - serializer.serialize_str(&self.to_string()) + serializer.serialize_str(&String::from(self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_invalid_date() { + let val = "2012-13-12"; + let date = BasicDate::try_from(val); + assert_eq!(date, Err(FejError::InvalidArgument)); } } 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..28ac98b 100644 --- a/src/ivago/mod.rs +++ b/src/ivago/mod.rs @@ -1,6 +1,4 @@ mod controller; -#[cfg(test)] -mod tests; use controller::structs::{BasicDate, PickupTime, Street}; use controller::{get_pickup_times, search_streets}; @@ -24,6 +22,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/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b1e2733 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,11 @@ +#![feature(proc_macro_hygiene, decl_macro)] + +#[macro_use] +extern crate rocket; + +// Route modules +pub mod ivago; + +// Helper modules +pub mod catchers; +pub mod errors; diff --git a/src/main.rs b/src/main.rs index 9452f9f..ad86d98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,7 @@ -#![feature(proc_macro_hygiene, decl_macro)] - #[macro_use] extern crate rocket; -// Route modules -mod catchers; -mod errors; -mod ivago; +use fej_lib::{catchers, ivago}; fn rocket() -> rocket::Rocket { rocket::ignite() diff --git a/src/ivago/tests.rs b/tests/ivago.rs similarity index 91% rename from src/ivago/tests.rs rename to tests/ivago.rs index 2b91e48..619b9c7 100644 --- a/src/ivago/tests.rs +++ b/tests/ivago.rs @@ -2,7 +2,7 @@ use rocket::http::Status; use rocket::local::Client; fn rocket() -> rocket::Rocket { - rocket::ignite().mount("/", super::routes()) + rocket::ignite().mount("/", fej_lib::ivago::routes()) } /// Test 404 response