diff --git a/Cargo.lock b/Cargo.lock index 5b96436..9c0d5ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,16 +174,6 @@ 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" @@ -350,7 +340,6 @@ name = "fej" version = "0.0.1" dependencies = [ "chrono", - "chrono-tz", "regex", "reqwest", "rocket", @@ -1021,15 +1010,6 @@ 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 0ebc525..6969693 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,26 +4,12 @@ version = "0.0.1" authors = ["Jef Roosens "] edition = "2018" -[lib] -name = "fej_lib" -src = "src/lib.rs" -test = true -bench = true -doc = true -doctest = true +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[[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 457e805..4a3ed8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,13 +13,11 @@ RUN apk update && apk add --no-cache openssl-dev build-base curl && \ COPY Cargo.toml Cargo.lock ./ COPY src/ ./src/ -# Run the tests, don't want no broken docker image -# And then finally, build the project +# 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 test && \ - RUSTFLAGS="-C target-feature=-crt-static" cargo build --release --bin fej +RUN RUSTFLAGS="-C target-feature=-crt-static" cargo build --release # Now, we create the actual image diff --git a/Makefile b/Makefile index ab50282..a1baf59 100644 --- a/Makefile +++ b/Makefile @@ -23,13 +23,13 @@ push: # Run run: - @ RUST_BACKTRACE=1 cargo run --bin fej + @ RUST_BACKTRACE=1 cargo run .PHONY: run # Testing test: - @ cargo test --no-fail-fast + @ cargo test .PHONY: test format: diff --git a/benches/ivago.rs b/benches/ivago.rs deleted file mode 100644 index 8b13789..0000000 --- a/benches/ivago.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/errors.rs b/src/errors.rs index dfb137f..cf78244 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,7 +1,5 @@ -// 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, @@ -22,9 +20,3 @@ 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 d2721f9..0a9bd3d 100644 --- a/src/ivago/controller/pickup_times.rs +++ b/src/ivago/controller/pickup_times.rs @@ -1,7 +1,5 @@ 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}; @@ -10,10 +8,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: &DateTime, - end_date: &DateTime, + street: Street, + number: u32, + start_date: BasicDate, + end_date: BasicDate, ) -> Result, FejError> { let client = reqwest::Client::builder().cookie_store(true).build()?; @@ -33,22 +31,20 @@ pub fn get_pickup_times( .query(&[ ("_format", "json"), ("type", ""), - ("start", &start_date.timestamp().to_string()), - ("end", &end_date.timestamp().to_string()), + ("start", &start_date.epoch().to_string()), + ("end", &end_date.epoch().to_string()), ]) .send()?; let data: Vec> = response.json()?; let mut output: Vec = Vec::new(); - 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())) - } + 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(), + )) } Ok(output) diff --git a/src/ivago/controller/search.rs b/src/ivago/controller/search.rs index e66652d..8c84113 100644 --- a/src/ivago/controller/search.rs +++ b/src/ivago/controller/search.rs @@ -13,17 +13,23 @@ 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()?; - // 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()) + 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) } diff --git a/src/ivago/controller/structs/basic_date.rs b/src/ivago/controller/structs/basic_date.rs index 97ef3e4..421d7df 100644 --- a/src/ivago/controller/structs/basic_date.rs +++ b/src/ivago/controller/structs/basic_date.rs @@ -1,17 +1,33 @@ -use crate::errors::FejError; -use chrono::{DateTime, NaiveDate, TimeZone}; -use chrono_tz::Europe::Brussels; -use chrono_tz::Tz; +use chrono::{FixedOffset, TimeZone}; +use regex::Regex; use rocket::http::RawStr; use rocket::request::FromFormValue; use serde::ser::Serializer; use serde::Serialize; use std::convert::TryFrom; -/// 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); +/// 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 allows us to use BasicDate as a query parameter in our routes. impl<'v> FromFormValue<'v> for BasicDate { @@ -25,25 +41,29 @@ impl<'v> FromFormValue<'v> for BasicDate { } } -/// 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)?, - )) +/// We need this when deserializing BasicDate. +impl ToString for BasicDate { + fn to_string(&self) -> String { + format!("{}-{}-{}", self.year, self.month, self.day) } } -impl From<&BasicDate> for String { - fn from(date: &BasicDate) -> String { - format!("{}", date.0.format("%Y-%m-%d")) +/// 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(), + }), + } } } @@ -52,18 +72,6 @@ impl Serialize for BasicDate { where S: Serializer, { - 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)); + serializer.serialize_str(&self.to_string()) } } diff --git a/src/ivago/controller/structs/street.rs b/src/ivago/controller/structs/street.rs index 1ca64ec..35f7e16 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<&Street> for String { - fn from(street: &Street) -> String { +impl From 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 28ac98b..78b1fc2 100644 --- a/src/ivago/mod.rs +++ b/src/ivago/mod.rs @@ -1,4 +1,6 @@ mod controller; +#[cfg(test)] +mod tests; use controller::structs::{BasicDate, PickupTime, Street}; use controller::{get_pickup_times, search_streets}; @@ -22,9 +24,6 @@ pub fn route_get_pickup_times( end_date: BasicDate, ) -> Result>, Status> { Ok(Json(get_pickup_times( - &street, - &number, - &start_date.0, - &end_date.0, + street, number, start_date, end_date, )?)) } diff --git a/tests/ivago.rs b/src/ivago/tests.rs similarity index 91% rename from tests/ivago.rs rename to src/ivago/tests.rs index 619b9c7..2b91e48 100644 --- a/tests/ivago.rs +++ b/src/ivago/tests.rs @@ -2,7 +2,7 @@ use rocket::http::Status; use rocket::local::Client; fn rocket() -> rocket::Rocket { - rocket::ignite().mount("/", fej_lib::ivago::routes()) + rocket::ignite().mount("/", super::routes()) } /// Test 404 response diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index b1e2733..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -#![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 ad86d98..9452f9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,12 @@ +#![feature(proc_macro_hygiene, decl_macro)] + #[macro_use] extern crate rocket; -use fej_lib::{catchers, ivago}; +// Route modules +mod catchers; +mod errors; +mod ivago; fn rocket() -> rocket::Rocket { rocket::ignite()