Compare commits
No commits in common. "2e73d88ae9bdb207b3340ab6bf1f5a4e680c15d0" and "5dd2b3a8786aa4d61a012345e506fa7a2abfaa8b" have entirely different histories.
2e73d88ae9
...
5dd2b3a878
14 changed files with 88 additions and 130 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
16
Cargo.toml
16
Cargo.toml
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
4
Makefile
4
Makefile
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)?))
|
)?))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
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]
|
#[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()
|
||||||
|
|
|
||||||
Reference in a new issue