Compare commits
3 Commits
5dd2b3a878
...
2e73d88ae9
Author | SHA1 | Date |
---|---|---|
Jef Roosens | 2e73d88ae9 | |
Jef Roosens | e78de73d83 | |
Jef Roosens | dd1efaa34d |
|
@ -174,6 +174,16 @@ 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"
|
||||||
|
@ -340,6 +350,7 @@ name = "fej"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
|
@ -1010,6 +1021,15 @@ 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,12 +4,26 @@ version = "0.0.1"
|
||||||
authors = ["Jef Roosens <roosensjef@gmail.com>"]
|
authors = ["Jef Roosens <roosensjef@gmail.com>"]
|
||||||
edition = "2018"
|
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]
|
[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,11 +13,13 @@ 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/
|
||||||
|
|
||||||
# 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
|
# 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 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
|
# Now, we create the actual image
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -23,13 +23,13 @@ push:
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
run:
|
run:
|
||||||
@ RUST_BACKTRACE=1 cargo run
|
@ RUST_BACKTRACE=1 cargo run --bin fej
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
test:
|
test:
|
||||||
@ cargo test
|
@ cargo test --no-fail-fast
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
|
|
||||||
format:
|
format:
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
// 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,
|
||||||
|
@ -20,3 +22,9 @@ impl From<reqwest::Error> for FejError {
|
||||||
FejError::FailedRequest
|
FejError::FailedRequest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<chrono::ParseError> for FejError {
|
||||||
|
fn from(_: chrono::ParseError) -> FejError {
|
||||||
|
FejError::InvalidArgument
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
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};
|
||||||
|
@ -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";
|
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: BasicDate,
|
start_date: &DateTime<Tz>,
|
||||||
end_date: BasicDate,
|
end_date: &DateTime<Tz>,
|
||||||
) -> 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()?;
|
||||||
|
|
||||||
|
@ -31,20 +33,22 @@ pub fn get_pickup_times(
|
||||||
.query(&[
|
.query(&[
|
||||||
("_format", "json"),
|
("_format", "json"),
|
||||||
("type", ""),
|
("type", ""),
|
||||||
("start", &start_date.epoch().to_string()),
|
("start", &start_date.timestamp().to_string()),
|
||||||
("end", &end_date.epoch().to_string()),
|
("end", &end_date.timestamp().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.iter() {
|
for map in data
|
||||||
output.push(PickupTime::new(
|
.iter()
|
||||||
// TODO should I check here if the parsing worked?
|
.filter(|m| m.contains_key("date") && m.contains_key("label"))
|
||||||
BasicDate::try_from(map.get("date").unwrap().as_str()).unwrap(),
|
{
|
||||||
map.get("label").unwrap().to_string(),
|
// 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)
|
Ok(output)
|
||||||
|
|
|
@ -13,23 +13,17 @@ 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()?;
|
||||||
|
|
||||||
let mut output: Vec<Street> = Vec::new();
|
// This is pretty cool, filter_map first does get() on all the maps, and
|
||||||
|
// then filters out any None values
|
||||||
// We iterate over every item and extract the needed data
|
// Then, we do the same thing for streets
|
||||||
for map in data.iter() {
|
Ok(data
|
||||||
if let Some(value) = map.get("value") {
|
.iter()
|
||||||
match Street::try_from(value.as_str()) {
|
.filter_map(|m| m.get("value"))
|
||||||
Ok(street) => output.push(street),
|
.filter_map(|v| Street::try_from(v.as_str()).ok())
|
||||||
Err(_) => continue,
|
.collect())
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(output)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,17 @@
|
||||||
use chrono::{FixedOffset, TimeZone};
|
use crate::errors::FejError;
|
||||||
use regex::Regex;
|
use chrono::{DateTime, NaiveDate, TimeZone};
|
||||||
|
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;
|
||||||
|
|
||||||
/// Represents a very simple Timezoneless date. Considering the timezone will
|
/// This class is a simple wrapper around chrono's DateTime. Its sole purpose
|
||||||
/// always be CEST (aka Belgium's timezone), this is good enough. I use this
|
/// is to avoid error E0117.
|
||||||
/// instead of a NaiveDate to avoid E0117.
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct BasicDate {
|
pub struct BasicDate(pub DateTime<Tz>);
|
||||||
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 {
|
||||||
|
@ -41,30 +25,26 @@ 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.
|
/// This is used to serialize BasicDate.
|
||||||
impl TryFrom<&str> for BasicDate {
|
impl TryFrom<&str> for BasicDate {
|
||||||
type Error = ();
|
type Error = FejError;
|
||||||
|
|
||||||
fn try_from(s: &str) -> Result<BasicDate, Self::Error> {
|
fn try_from(s: &str) -> Result<BasicDate, Self::Error> {
|
||||||
let re = Regex::new(r"^(\d{4})-(\d{2})-(\d{2})$").unwrap();
|
let naive_date = NaiveDate::parse_from_str(s, "%Y-%m-%d")?;
|
||||||
|
|
||||||
match re.captures(s) {
|
Ok(BasicDate(
|
||||||
None => Err(()),
|
Brussels
|
||||||
Some(caps) => Ok(BasicDate {
|
.from_local_datetime(&naive_date.and_hms(0, 0, 0))
|
||||||
// TODO change this to ? operator if possible
|
.single()
|
||||||
year: caps.get(1).unwrap().as_str().parse().unwrap(),
|
.ok_or(FejError::InvalidArgument)?,
|
||||||
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"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for BasicDate {
|
impl Serialize for BasicDate {
|
||||||
|
@ -72,6 +52,18 @@ impl Serialize for BasicDate {
|
||||||
where
|
where
|
||||||
S: Serializer,
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,6 +1,4 @@
|
||||||
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};
|
||||||
|
@ -24,6 +22,9 @@ 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, number, start_date, end_date,
|
&street,
|
||||||
|
&number,
|
||||||
|
&start_date.0,
|
||||||
|
&end_date.0,
|
||||||
)?))
|
)?))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -1,12 +1,7 @@
|
||||||
#![feature(proc_macro_hygiene, decl_macro)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
|
|
||||||
// Route modules
|
use fej_lib::{catchers, ivago};
|
||||||
mod catchers;
|
|
||||||
mod errors;
|
|
||||||
mod ivago;
|
|
||||||
|
|
||||||
fn rocket() -> rocket::Rocket {
|
fn rocket() -> rocket::Rocket {
|
||||||
rocket::ignite()
|
rocket::ignite()
|
||||||
|
|
|
@ -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("/", super::routes())
|
rocket::ignite().mount("/", fej_lib::ivago::routes())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test 404 response
|
/// Test 404 response
|
Loading…
Reference in New Issue