Compare commits
	
		
			No commits in common. "2e73d88ae9bdb207b3340ab6bf1f5a4e680c15d0" and "5dd2b3a8786aa4d61a012345e506fa7a2abfaa8b" have entirely different histories. 
		
	
	
		
			2e73d88ae9
			...
			5dd2b3a878
		
	
		| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										16
									
								
								Cargo.toml
								
								
								
								
							
							
						
						
									
										16
									
								
								Cargo.toml
								
								
								
								
							| 
						 | 
				
			
			@ -4,26 +4,12 @@ version = "0.0.1"
 | 
			
		|||
authors = ["Jef Roosens <roosensjef@gmail.com>"]
 | 
			
		||||
edition = "2018"
 | 
			
		||||
 | 
			
		||||
[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
 | 
			
		||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
rocket = "0.4.7"
 | 
			
		||||
serde = "1.0.124"
 | 
			
		||||
chrono = "0.4.19"
 | 
			
		||||
chrono-tz = "0.5.3"
 | 
			
		||||
regex = "1.4.5"
 | 
			
		||||
 | 
			
		||||
[dependencies.reqwest]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										4
									
								
								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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
 | 
			
		||||
| 
						 | 
				
			
			@ -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<reqwest::Error> for FejError {
 | 
			
		|||
        FejError::FailedRequest
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<chrono::ParseError> for FejError {
 | 
			
		||||
    fn from(_: chrono::ParseError) -> FejError {
 | 
			
		||||
        FejError::InvalidArgument
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Tz>,
 | 
			
		||||
    end_date: &DateTime<Tz>,
 | 
			
		||||
    street: Street,
 | 
			
		||||
    number: u32,
 | 
			
		||||
    start_date: BasicDate,
 | 
			
		||||
    end_date: BasicDate,
 | 
			
		||||
) -> Result<Vec<PickupTime>, 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<HashMap<String, String>> = response.json()?;
 | 
			
		||||
 | 
			
		||||
    let mut output: Vec<PickupTime> = 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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Vec<Street>, FejError> {
 | 
			
		||||
    let client = reqwest::Client::new();
 | 
			
		||||
    let response = client.get(SEARCH_URL).query(&[("q", street_name)]).send()?;
 | 
			
		||||
    let data: Vec<HashMap<String, String>> = 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<Street> = 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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Tz>);
 | 
			
		||||
/// 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<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)?,
 | 
			
		||||
        ))
 | 
			
		||||
/// 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<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
 | 
			
		||||
        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())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,8 +10,8 @@ pub struct Street {
 | 
			
		|||
    pub city: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&Street> for String {
 | 
			
		||||
    fn from(street: &Street) -> String {
 | 
			
		||||
impl From<Street> for String {
 | 
			
		||||
    fn from(street: Street) -> String {
 | 
			
		||||
        format!("{} ({})", street.name, street.city)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Json<Vec<PickupTime>>, Status> {
 | 
			
		||||
    Ok(Json(get_pickup_times(
 | 
			
		||||
        &street,
 | 
			
		||||
        &number,
 | 
			
		||||
        &start_date.0,
 | 
			
		||||
        &end_date.0,
 | 
			
		||||
        street, number, start_date, end_date,
 | 
			
		||||
    )?))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
							
								
								
									
										11
									
								
								src/lib.rs
								
								
								
								
							
							
						
						
									
										11
									
								
								src/lib.rs
								
								
								
								
							| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue