Merge branch 'develop'
commit
dddde5074a
|
@ -6,12 +6,12 @@ make lint &> /dev/null 2>&1 || {
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
branch=`git rev-parse --abbrev-ref HEAD`
|
# branch=`git rev-parse --abbrev-ref HEAD`
|
||||||
|
|
||||||
# TODO should we add release branches here as well?
|
# # TODO should we add release branches here as well?
|
||||||
if [[ "$branch" =~ ^master|develop$ ]]; then
|
# if [[ "$branch" =~ ^master|develop$ ]]; then
|
||||||
make test > /dev/null 2>&1 || {
|
# make test > /dev/null 2>&1 || {
|
||||||
>&2 echo "Tests failed. check 'make test' for more info.";
|
# >&2 echo "Tests failed. check 'make test' for more info.";
|
||||||
exit 1;
|
# exit 1;
|
||||||
}
|
# }
|
||||||
fi
|
# fi
|
||||||
|
|
37
Dockerfile
37
Dockerfile
|
@ -1,37 +0,0 @@
|
||||||
# syntax = docker/dockerfile:1.2
|
|
||||||
|
|
||||||
# We use a multi-stage build to end up with a very small final image
|
|
||||||
FROM alpine:latest AS builder
|
|
||||||
|
|
||||||
ENV PATH "$PATH:/root/.cargo/bin"
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
# Install build dependencies, rustup & rust's nightly build & toolchain
|
|
||||||
RUN apk update && apk add --no-cache openssl-dev build-base curl && \
|
|
||||||
{ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain nightly; }
|
|
||||||
|
|
||||||
# Copy source code over to builder
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY src/ ./src/
|
|
||||||
|
|
||||||
# Run the tests, don't want no broken docker image
|
|
||||||
# And then 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
|
|
||||||
# NOTE: cargo install auto-appends bin to the path
|
|
||||||
RUN --mount=type=cache,target=/usr/src/app/target RUSTFLAGS="-C target-feature=-crt-static" cargo test && \
|
|
||||||
RUSTFLAGS="-C target-feature=-crt-static" cargo install --path . --bin fej --root /usr/local
|
|
||||||
|
|
||||||
|
|
||||||
# Now, we create the actual image
|
|
||||||
FROM alpine:latest
|
|
||||||
|
|
||||||
# Install some dynamic libraries needed for everything to work
|
|
||||||
RUN apk update && apk add --no-cache openssl libgcc
|
|
||||||
|
|
||||||
# Copy binary over to final image
|
|
||||||
COPY --from=builder /usr/local/bin/fej /usr/local/bin/fej
|
|
||||||
|
|
||||||
CMD ["/usr/local/bin/fej"]
|
|
25
Makefile
25
Makefile
|
@ -1,35 +1,36 @@
|
||||||
IMAGE := chewingbever/fej
|
|
||||||
|
|
||||||
|
|
||||||
all: debug
|
all: debug
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
|
|
||||||
# Builds
|
# Builds
|
||||||
debug:
|
debug:
|
||||||
@ cargo build
|
@ ./build -m dev
|
||||||
.PHONY: debug
|
.PHONY: debug
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@ cargo build --release
|
@ ./build -m rel
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
|
|
||||||
image: Dockerfile
|
|
||||||
@ ./build '$(IMAGE)'
|
|
||||||
.PHONY: image
|
|
||||||
|
|
||||||
push:
|
push:
|
||||||
@ ./build '$(IMAGE)' push
|
@ ./build -m prod -a push
|
||||||
.PHONY: push
|
.PHONY: push
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
run:
|
run:
|
||||||
@ RUST_BACKTRACE=1 cargo run --bin fej
|
@ ./build -m dev -a run
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
|
|
||||||
|
stop:
|
||||||
|
@ docker stop -t 2 fej
|
||||||
|
.PHONY: stop
|
||||||
|
|
||||||
|
logs:
|
||||||
|
@ docker logs -f fej
|
||||||
|
.PHONY: logs
|
||||||
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
test:
|
test:
|
||||||
@ cargo test --no-fail-fast
|
@ ./build -m dev -a run -l -- test --no-fail-fast
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
|
|
||||||
format:
|
format:
|
||||||
|
|
76
build
76
build
|
@ -1,18 +1,27 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Simple guard to check input args
|
image="chewingbever/fej"
|
||||||
[[ $# -eq 1 ]] || [[ $# -eq 2 ]] || {
|
# Should be either dev or rel
|
||||||
>&2 echo "Usage: ./build IMAGE [ACTION]"
|
mode="dev"
|
||||||
exit 1
|
action=""
|
||||||
}
|
attach="--detach"
|
||||||
|
|
||||||
|
while getopts ":i:m:a:l" c; do
|
||||||
|
case $c in
|
||||||
|
i ) image="$OPTARG" ;;
|
||||||
|
m ) mode="$OPTARG" ;;
|
||||||
|
a ) action="$OPTARG" ;;
|
||||||
|
l ) attach="" ;;
|
||||||
|
? ) exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
shift $((OPTIND-1))
|
||||||
|
|
||||||
# Extract current version from Cargo.toml & get current branch
|
# Extract current version from Cargo.toml & get current branch
|
||||||
patch_version="$(grep -Po '(?<=version = ").*(?=")' Cargo.toml | head -n1)"
|
patch_version=`grep -Po '(?<=version = ").*(?=")' Cargo.toml | head -n1`
|
||||||
major_version="$(echo "$patch_version" |
|
major_version=`echo "$patch_version" | sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+)/\1/'`
|
||||||
sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+)/\1/')"
|
minor_version=`echo "$patch_version" | sed -E 's/([0-9]+).([0-9]+).([0-9]+)/\1.\2/'`
|
||||||
minor_version="$(echo "$patch_version" |
|
branch=`git rev-parse --abbrev-ref HEAD`
|
||||||
sed -E 's/([0-9]+).([0-9]+).([0-9]+)/\1.\2/')"
|
|
||||||
branch="$(git branch --show-current)"
|
|
||||||
|
|
||||||
if [[ "$branch" = "master" ]]; then
|
if [[ "$branch" = "master" ]]; then
|
||||||
tags=("$patch_version" "$minor_version" "$major_version" "latest")
|
tags=("$patch_version" "$minor_version" "$major_version" "latest")
|
||||||
|
@ -25,31 +34,58 @@ else
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run the actual build command
|
# First, we build the builder
|
||||||
DOCKER_BUILDKIT=1 docker build -t "$1:$tags" .
|
DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile.builder -t "$image-builder:latest" .
|
||||||
|
|
||||||
if [[ "$2" = push ]]; then
|
# Run the actual build command
|
||||||
|
if [ "$mode" = "rel" ]; then
|
||||||
|
DOCKER_BUILDKIT=1 docker build -t "$image:$tags" -f docker/Dockerfile.rel .
|
||||||
|
|
||||||
|
elif [[ "$mode" = "dev" ]]; then
|
||||||
|
DOCKER_BUILDKIT=1 docker build -t "$image-dev:$tags" -f docker/Dockerfile.dev .
|
||||||
|
|
||||||
|
else
|
||||||
|
>&2 echo "Invalid mode."
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$action" = push ]]; then
|
||||||
[[ "$branch" =~ ^develop|master$ ]] || {
|
[[ "$branch" =~ ^develop|master$ ]] || {
|
||||||
>&2 echo "You can only push from develop or master."
|
>&2 echo "You can only push from develop or master."
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[ "$mode" = "rel" ]] || {
|
||||||
|
>&2 echo "You can only push release builds."
|
||||||
|
exit 3
|
||||||
|
}
|
||||||
|
|
||||||
for tag in "${tags[@]}"; do
|
for tag in "${tags[@]}"; do
|
||||||
# Create the tag
|
# Create the tag
|
||||||
docker tag "$1:$tags" "$1:$tag"
|
docker tag "$image:$tags" "$image:$tag"
|
||||||
|
|
||||||
# Push the tag
|
# Push the tag
|
||||||
docker push "$1:$tag"
|
docker push "$image:$tag"
|
||||||
|
|
||||||
# Remove the tag again, if it's not the main tag
|
# Remove the tag again, if it's not the main tag
|
||||||
[[ "$tag" != "$tags" ]] && docker rmi "$1:$tag"
|
[[ "$tag" != "$tags" ]] && docker rmi "$image:$tag"
|
||||||
done
|
done
|
||||||
|
|
||||||
elif [[ "$2" = run ]]; then
|
elif [[ "$action" = run ]]; then
|
||||||
docker run \
|
if [[ "$mode" = "dev" ]]; then
|
||||||
|
# Create caching volumes if needed (they need to be named)
|
||||||
|
docker volume create fej_build-cache
|
||||||
|
docker volume create fej_registry-cache
|
||||||
|
|
||||||
|
flags="-v fej_build-cache:/usr/src/app/target -v fej_registry-cache:/root/.cargo/registry"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run $attach $flags \
|
||||||
--rm \
|
--rm \
|
||||||
--interactive \
|
--interactive \
|
||||||
--tty \
|
--tty \
|
||||||
--publish 8000:8000 \
|
--publish 8000:8000 \
|
||||||
"$1:$tags"
|
--name fej \
|
||||||
|
"$image$([[ "$mode" != "rel" ]] && echo "-dev"):$tags" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# We use a multi-stage build to end up with a very small final image
|
||||||
|
FROM alpine:latest AS builder
|
||||||
|
|
||||||
|
ARG MODE
|
||||||
|
ARG RUN_TESTS
|
||||||
|
|
||||||
|
ENV PATH "$PATH:/root/.cargo/bin"
|
||||||
|
# Needed for proper compiling of openssl-dev
|
||||||
|
ENV RUSTFLAGS="-C target-feature=-crt-static"
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Install build dependencies, rustup & rust's nightly build & toolchain
|
||||||
|
RUN apk update && apk add --no-cache openssl-dev build-base curl && \
|
||||||
|
{ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain nightly; }
|
||||||
|
|
||||||
|
# Copy source code over to builder
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY src/ ./src/
|
|
@ -0,0 +1,6 @@
|
||||||
|
FROM chewingbever/fej-builder:latest
|
||||||
|
|
||||||
|
ENV RUST_BACKTRACE=1
|
||||||
|
|
||||||
|
ENTRYPOINT ["cargo"]
|
||||||
|
CMD ["run"]
|
|
@ -0,0 +1,22 @@
|
||||||
|
FROM chewingbever/fej-builder:latest AS builder
|
||||||
|
|
||||||
|
# And then 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
|
||||||
|
# NOTE: cargo install auto-appends bin to the path
|
||||||
|
RUN --mount=type=cache,target=/usr/src/app/target \
|
||||||
|
--mount=type=cache,target=/root/.cargo/registry \
|
||||||
|
cargo install --path . --bin fej --root /usr/local
|
||||||
|
|
||||||
|
|
||||||
|
# Now, we create the actual image
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install some dynamic libraries needed for everything to work
|
||||||
|
RUN apk update && apk add --no-cache openssl libgcc
|
||||||
|
|
||||||
|
# Copy binary over to final image
|
||||||
|
COPY --from=builder /usr/local/bin/fej /usr/local/bin/fej
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/fej"]
|
|
@ -1,6 +1,9 @@
|
||||||
// I can probably do this way easier using an external crate, I should do that
|
// I can probably do this way easier using an external crate, I should do that
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
|
|
||||||
|
/// Represents any general error that the API can encounter during execution.
|
||||||
|
/// It allows us to more easily process errors, and hopefully allows for
|
||||||
|
/// more clear error messages.
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum FejError {
|
pub enum FejError {
|
||||||
InvalidArgument,
|
InvalidArgument,
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Ivago
|
|
||||||
|
|
||||||
This part of the API is a wrapper around the Ivago website (Ivago being the
|
|
||||||
company that collects the trash in my city). Their site isn't exactly RESTful,
|
|
||||||
so this endpoint simply wraps it in a RESTful wrapper.
|
|
|
@ -20,15 +20,14 @@ const BASE_URL: &str = "https://www.ivago.be/nl/particulier/afval/ophaling";
|
||||||
/// Endpoint for the actual calendar output
|
/// Endpoint for the actual calendar output
|
||||||
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";
|
||||||
|
|
||||||
/// Searches the Ivago API for streets in the given city
|
/// Searches the Ivago API for streets in the given city.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `street` - name of the street
|
/// * `search_term` - Search term to use to look for streets
|
||||||
/// * `city` - city the street is in
|
pub fn search_streets(search_term: &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", search_term)]).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
|
// This is pretty cool, filter_map first does get() on all the maps, and
|
||||||
|
@ -41,14 +40,14 @@ pub fn search_streets(street_name: &str) -> Result<Vec<Street>, FejError> {
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the pickup times for the various trash types
|
/// Returns the pickup times for the various trash types.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `street` - desired street
|
/// * `street` - Street to look up
|
||||||
/// * `number` - house number
|
/// * `number` - House number in given street
|
||||||
/// * `start_date` - earliest date for the results
|
/// * `start_date` - Earliest date for the results
|
||||||
/// * `end_date` - latest date for the results
|
/// * `end_date` - Latest date for the results
|
||||||
pub fn get_pickup_times(
|
pub fn get_pickup_times(
|
||||||
street: &Street,
|
street: &Street,
|
||||||
number: &u32,
|
number: &u32,
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
use super::BasicDate;
|
use super::BasicDate;
|
||||||
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
||||||
|
|
||||||
/// Represents a pickup time instance. All fields are a direct map of the
|
/// Represents a date when a pickup will occur. Label describes which type of
|
||||||
/// original API
|
/// trash will be picked up.
|
||||||
pub struct PickupTime {
|
pub struct PickupTime {
|
||||||
date: BasicDate,
|
date: BasicDate,
|
||||||
label: String,
|
label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PickupTime {
|
impl PickupTime {
|
||||||
|
/// Creates a new PickupTime instance.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `date` - Date of pickup time
|
||||||
|
/// * `label` - Type of trash
|
||||||
pub fn new(date: BasicDate, label: String) -> PickupTime {
|
pub fn new(date: BasicDate, label: String) -> PickupTime {
|
||||||
PickupTime {
|
PickupTime {
|
||||||
date: date,
|
date: date,
|
||||||
|
|
|
@ -4,7 +4,7 @@ use rocket::request::FromFormValue;
|
||||||
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
/// Represents a street
|
/// Represents a street in a given city
|
||||||
pub struct Street {
|
pub struct Street {
|
||||||
name: String,
|
name: String,
|
||||||
city: String,
|
city: String,
|
||||||
|
@ -76,6 +76,7 @@ impl<'v> FromFormValue<'v> for Street {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// Tests the conversion to string
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_string() {
|
fn test_to_string() {
|
||||||
let street = Street::new(String::from("testname"), String::from("city"));
|
let street = Street::new(String::from("testname"), String::from("city"));
|
||||||
|
|
|
@ -9,11 +9,26 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||||
routes![route_search_streets, route_get_pickup_times]
|
routes![route_search_streets, route_get_pickup_times]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/search?<street>")]
|
/// This route handles the Ivago search endpoint. It returns a list of streets,
|
||||||
pub fn route_search_streets(street: String) -> Result<Json<Vec<Street>>, Status> {
|
/// consisting of a street name & a city.
|
||||||
Ok(Json(search_streets(street.as_str())?))
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `search_term` - Search term to use to look for streets
|
||||||
|
#[get("/search?<search_term>")]
|
||||||
|
pub fn route_search_streets(search_term: String) -> Result<Json<Vec<Street>>, Status> {
|
||||||
|
Ok(Json(search_streets(search_term.as_str())?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles returning of pickup times for a specific address. It returns a list
|
||||||
|
/// of pickup times, containing a date and a description of the trash type.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `street` - Street to look up
|
||||||
|
/// * `number` - House number in the given street
|
||||||
|
/// * `start_date` - Earliest date that can be returned
|
||||||
|
/// * `end_date` - Latest date that can be returned
|
||||||
#[get("/?<street>&<number>&<start_date>&<end_date>")]
|
#[get("/?<street>&<number>&<start_date>&<end_date>")]
|
||||||
pub fn route_get_pickup_times(
|
pub fn route_get_pickup_times(
|
||||||
street: Street,
|
street: Street,
|
||||||
|
|
Loading…
Reference in New Issue