Compare commits

..

No commits in common. "2e73d88ae9bdb207b3340ab6bf1f5a4e680c15d0" and "5dd2b3a8786aa4d61a012345e506fa7a2abfaa8b" have entirely different histories.

14 changed files with 88 additions and 130 deletions

20
Cargo.lock generated
View File

@ -174,16 +174,6 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "cipher" name = "cipher"
version = "0.2.5" version = "0.2.5"
@ -350,7 +340,6 @@ name = "fej"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-tz",
"regex", "regex",
"reqwest", "reqwest",
"rocket", "rocket",
@ -1021,15 +1010,6 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "parse-zoneinfo"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
dependencies = [
"regex",
]
[[package]] [[package]]
name = "pear" name = "pear"
version = "0.1.4" version = "0.1.4"

View File

@ -4,26 +4,12 @@ version = "0.0.1"
authors = ["Jef Roosens <roosensjef@gmail.com>"] authors = ["Jef Roosens <roosensjef@gmail.com>"]
edition = "2018" edition = "2018"
[lib] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
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] [dependencies]
rocket = "0.4.7" rocket = "0.4.7"
serde = "1.0.124" serde = "1.0.124"
chrono = "0.4.19" chrono = "0.4.19"
chrono-tz = "0.5.3"
regex = "1.4.5" regex = "1.4.5"
[dependencies.reqwest] [dependencies.reqwest]

View File

@ -13,13 +13,11 @@ RUN apk update && apk add --no-cache openssl-dev build-base curl && \
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
COPY src/ ./src/ COPY src/ ./src/
# Run the tests, don't want no broken docker image # Finally, build the project
# And then finally, build the project
# Thank the lords that this article exists # Thank the lords that this article exists
# https://users.rust-lang.org/t/sigsegv-with-program-linked-against-openssl-in-an-alpine-container/52172 # 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 # TODO add what these flags do & why they work
RUN RUSTFLAGS="-C target-feature=-crt-static" cargo test && \ RUN RUSTFLAGS="-C target-feature=-crt-static" cargo build --release
RUSTFLAGS="-C target-feature=-crt-static" cargo build --release --bin fej
# Now, we create the actual image # Now, we create the actual image

View File

@ -23,13 +23,13 @@ push:
# Run # Run
run: run:
@ RUST_BACKTRACE=1 cargo run --bin fej @ RUST_BACKTRACE=1 cargo run
.PHONY: run .PHONY: run
# Testing # Testing
test: test:
@ cargo test --no-fail-fast @ cargo test
.PHONY: test .PHONY: test
format: format:

View File

@ -1 +0,0 @@

View File

@ -1,7 +1,5 @@
// I can probably do this way easier using an external crate, I should do that
use rocket::http::Status; use rocket::http::Status;
#[derive(Debug, PartialEq)]
pub enum FejError { pub enum FejError {
InvalidArgument, InvalidArgument,
FailedRequest, FailedRequest,
@ -22,9 +20,3 @@ impl From<reqwest::Error> for FejError {
FejError::FailedRequest FejError::FailedRequest
} }
} }
impl From<chrono::ParseError> for FejError {
fn from(_: chrono::ParseError) -> FejError {
FejError::InvalidArgument
}
}

View File

@ -1,7 +1,5 @@
use super::structs::{BasicDate, PickupTime, Street}; use super::structs::{BasicDate, PickupTime, Street};
use crate::errors::FejError; use crate::errors::FejError;
use chrono::DateTime;
use chrono_tz::Tz;
use reqwest::blocking as reqwest; use reqwest::blocking as reqwest;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::{From, TryFrom}; 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"; const CAL_URL: &str = "https://www.ivago.be/nl/particulier/garbage/pick-up/pickups";
pub fn get_pickup_times( pub fn get_pickup_times(
street: &Street, street: Street,
number: &u32, number: u32,
start_date: &DateTime<Tz>, start_date: BasicDate,
end_date: &DateTime<Tz>, end_date: BasicDate,
) -> Result<Vec<PickupTime>, FejError> { ) -> Result<Vec<PickupTime>, FejError> {
let client = reqwest::Client::builder().cookie_store(true).build()?; let client = reqwest::Client::builder().cookie_store(true).build()?;
@ -33,22 +31,20 @@ pub fn get_pickup_times(
.query(&[ .query(&[
("_format", "json"), ("_format", "json"),
("type", ""), ("type", ""),
("start", &start_date.timestamp().to_string()), ("start", &start_date.epoch().to_string()),
("end", &end_date.timestamp().to_string()), ("end", &end_date.epoch().to_string()),
]) ])
.send()?; .send()?;
let data: Vec<HashMap<String, String>> = response.json()?; let data: Vec<HashMap<String, String>> = response.json()?;
let mut output: Vec<PickupTime> = Vec::new(); let mut output: Vec<PickupTime> = Vec::new();
for map in data for map in data.iter() {
.iter() output.push(PickupTime::new(
.filter(|m| m.contains_key("date") && m.contains_key("label")) // TODO should I check here if the parsing worked?
{ BasicDate::try_from(map.get("date").unwrap().as_str()).unwrap(),
// Because we filtered the maps in the loop, we can safely us unwrap here map.get("label").unwrap().to_string(),
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) Ok(output)

View File

@ -13,17 +13,23 @@ const SEARCH_URL: &str = "https://www.ivago.be/nl/particulier/autocomplete/garba
/// ///
/// * `street` - name of the street /// * `street` - name of the street
/// * `city` - city the street is in /// * `city` - city the street is in
// TODO find out how to do this async
pub fn search_streets(street_name: &str) -> Result<Vec<Street>, FejError> { pub fn search_streets(street_name: &str) -> Result<Vec<Street>, FejError> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let response = client.get(SEARCH_URL).query(&[("q", street_name)]).send()?; let response = client.get(SEARCH_URL).query(&[("q", street_name)]).send()?;
let data: Vec<HashMap<String, String>> = response.json()?; let data: Vec<HashMap<String, String>> = response.json()?;
// This is pretty cool, filter_map first does get() on all the maps, and let mut output: Vec<Street> = Vec::new();
// then filters out any None values
// Then, we do the same thing for streets // We iterate over every item and extract the needed data
Ok(data for map in data.iter() {
.iter() if let Some(value) = map.get("value") {
.filter_map(|m| m.get("value")) match Street::try_from(value.as_str()) {
.filter_map(|v| Street::try_from(v.as_str()).ok()) Ok(street) => output.push(street),
.collect()) Err(_) => continue,
}
}
}
Ok(output)
} }

View File

@ -1,17 +1,33 @@
use crate::errors::FejError; use chrono::{FixedOffset, TimeZone};
use chrono::{DateTime, NaiveDate, TimeZone}; use regex::Regex;
use chrono_tz::Europe::Brussels;
use chrono_tz::Tz;
use rocket::http::RawStr; use rocket::http::RawStr;
use rocket::request::FromFormValue; use rocket::request::FromFormValue;
use serde::ser::Serializer; use serde::ser::Serializer;
use serde::Serialize; use serde::Serialize;
use std::convert::TryFrom; use std::convert::TryFrom;
/// This class is a simple wrapper around chrono's DateTime. Its sole purpose /// Represents a very simple Timezoneless date. Considering the timezone will
/// is to avoid error E0117. /// always be CEST (aka Belgium's timezone), this is good enough. I use this
#[derive(Debug, PartialEq)] /// instead of a NaiveDate to avoid E0117.
pub struct BasicDate(pub DateTime<Tz>); 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. /// This allows us to use BasicDate as a query parameter in our routes.
impl<'v> FromFormValue<'v> for BasicDate { impl<'v> FromFormValue<'v> for BasicDate {
@ -25,25 +41,29 @@ impl<'v> FromFormValue<'v> for BasicDate {
} }
} }
/// This is used to serialize BasicDate. /// We need this when deserializing BasicDate.
impl TryFrom<&str> for BasicDate { impl ToString for BasicDate {
type Error = FejError; fn to_string(&self) -> String {
format!("{}-{}-{}", self.year, self.month, self.day)
fn try_from(s: &str) -> Result<BasicDate, Self::Error> {
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)?,
))
} }
} }
impl From<&BasicDate> for String { /// This is used to serialize BasicDate.
fn from(date: &BasicDate) -> String { impl TryFrom<&str> for BasicDate {
format!("{}", date.0.format("%Y-%m-%d")) type Error = ();
fn try_from(s: &str) -> Result<BasicDate, Self::Error> {
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 where
S: Serializer, S: Serializer,
{ {
serializer.serialize_str(&String::from(self)) serializer.serialize_str(&self.to_string())
}
}
#[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));
} }
} }

View File

@ -10,8 +10,8 @@ pub struct Street {
pub city: String, pub city: String,
} }
impl From<&Street> for String { impl From<Street> for String {
fn from(street: &Street) -> String { fn from(street: Street) -> String {
format!("{} ({})", street.name, street.city) format!("{} ({})", street.name, street.city)
} }
} }

View File

@ -1,4 +1,6 @@
mod controller; mod controller;
#[cfg(test)]
mod tests;
use controller::structs::{BasicDate, PickupTime, Street}; use controller::structs::{BasicDate, PickupTime, Street};
use controller::{get_pickup_times, search_streets}; use controller::{get_pickup_times, search_streets};
@ -22,9 +24,6 @@ pub fn route_get_pickup_times(
end_date: BasicDate, end_date: BasicDate,
) -> Result<Json<Vec<PickupTime>>, Status> { ) -> Result<Json<Vec<PickupTime>>, Status> {
Ok(Json(get_pickup_times( Ok(Json(get_pickup_times(
&street, street, number, start_date, end_date,
&number,
&start_date.0,
&end_date.0,
)?)) )?))
} }

View File

@ -2,7 +2,7 @@ use rocket::http::Status;
use rocket::local::Client; use rocket::local::Client;
fn rocket() -> rocket::Rocket { fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", fej_lib::ivago::routes()) rocket::ignite().mount("/", super::routes())
} }
/// Test 404 response /// Test 404 response

View File

@ -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;

View File

@ -1,7 +1,12 @@
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
use fej_lib::{catchers, ivago}; // Route modules
mod catchers;
mod errors;
mod ivago;
fn rocket() -> rocket::Rocket { fn rocket() -> rocket::Rocket {
rocket::ignite() rocket::ignite()