Compare commits

..

4 Commits

Author SHA1 Message Date
477c74d5d2 chore: Reorganize files
All checks were successful
Deploy application / deploy (push) Successful in 32s
2025-09-21 13:04:18 +02:00
2326bb6e21 Add deploy pipeline
All checks were successful
Deploy application / deploy (push) Successful in 1m26s
2025-09-20 14:01:50 +02:00
9017888794 Containerfile and compose 2025-09-20 13:59:25 +02:00
a69b8a9c55 All day event 2025-09-20 13:06:21 +02:00
14 changed files with 251 additions and 166 deletions

View File

@ -0,0 +1,14 @@
name: Deploy application
on:
push:
branches: [ master ]
jobs:
deploy:
runs-on: host
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Run docker-compose
run: docker compose up -d --build

View File

@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run recurring-event-api" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="buildProfileId" value="dev" />
<option name="command" value="run --package recurring-event-api --bin recurring-event-api" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs />
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>

24
Cargo.lock generated
View File

@ -125,18 +125,6 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "caldendar-api"
version = "0.1.0"
dependencies = [
"axum",
"chrono",
"icalendar",
"serde",
"serde_json",
"tokio",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.37" version = "1.2.37"
@ -537,6 +525,18 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "recurring-event-api"
version = "0.1.0"
dependencies = [
"axum",
"chrono",
"icalendar",
"serde",
"serde_json",
"tokio",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.26" version = "0.1.26"

View File

@ -1,5 +1,5 @@
[package] [package]
name = "caldendar-api" name = "recurring-event-api"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"

27
Containerfile Normal file
View File

@ -0,0 +1,27 @@
# Use a Rust base image with Cargo installed
FROM rust:1.90 AS builder
LABEL authors="Martin Berg Alstad"
# Set the working directory inside the container
WORKDIR /usr/src/app
# Now copy the source code
COPY Cargo.toml Cargo.lock ./
COPY ./src ./src
# Build your application
RUN cargo build --release
# Start a new stage to create a smaller image without unnecessary build dependencies
FROM debian:trixie-slim
# Set the working directory
WORKDIR /usr/src/app
# Copy the built binary from the previous stage
COPY --from=builder /usr/src/app/target/release/recurring-event-api ./
EXPOSE 8000
# Command to run the application
ENTRYPOINT ["./recurring-event-api"]

10
compose.yaml Normal file
View File

@ -0,0 +1,10 @@
services:
recurring-event-api:
restart: unless-stopped
container_name: recurring-event-api
image: recurring-event-api
build:
dockerfile: Containerfile
context: .
ports:
- "8095:8000"

46
src/handler/ics.rs Normal file
View File

@ -0,0 +1,46 @@
use crate::handler::EventQuery;
use crate::service::date_service::get_dates;
use axum::extract::Query;
use axum::http::header::CONTENT_TYPE;
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use chrono::TimeDelta;
use icalendar::{Calendar, Class, Component, Event, EventLike};
const TEXT_CALENDAR: &'static str = "text/calendar";
pub(super) fn router() -> Router {
Router::new().route("/ics", get(get_ics))
}
async fn get_ics(Query(query): Query<EventQuery>) -> impl IntoResponse {
let start = query.start_date_time;
let end = query
.end_date
.unwrap_or_else(|| (start + TimeDelta::days(365)).date());
let mut calendar = Calendar::new();
calendar.name(&query.title);
get_dates(
start,
end,
query.recurring,
query.avoid_weekends.unwrap_or(false),
)
.into_iter()
.for_each(|date_time| {
let mut event = Event::new();
if query.all_day {
event.all_day(date_time.date());
} else {
event.starts(date_time);
};
event.summary(&query.title).class(Class::Private);
calendar.push(event);
});
Response::builder()
.header(CONTENT_TYPE, TEXT_CALENDAR)
.body(calendar.done().to_string())
.unwrap()
}

24
src/handler/index.rs Normal file
View File

@ -0,0 +1,24 @@
use crate::handler::EventQuery;
use crate::service::date_service::get_dates;
use axum::extract::Query;
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Json, Router};
use chrono::TimeDelta;
pub(super) fn router() -> Router {
Router::new().route("/", get(get_calendar))
}
async fn get_calendar(Query(query): Query<EventQuery>) -> impl IntoResponse {
let start = query.start_date_time;
let end = query
.end_date
.unwrap_or_else(|| (start + TimeDelta::days(365)).date());
Json(get_dates(
start,
end,
query.recurring,
query.avoid_weekends.unwrap_or(false),
))
}

27
src/handler/mod.rs Normal file
View File

@ -0,0 +1,27 @@
use crate::model::models::{Move, Recurring};
use axum::Router;
use chrono::{NaiveDate, NaiveDateTime};
use serde::Deserialize;
mod ics;
mod index;
pub(crate) fn router() -> Router {
Router::new().merge(index::router()).merge(ics::router())
}
#[derive(Deserialize)]
struct EventQuery {
/// Start date
start_date_time: NaiveDateTime,
/// Recurring date
recurring: Recurring,
// If set, will ignore time on start_date
all_day: bool,
/// Title of the event
title: String,
// Optionals
end_date: Option<NaiveDate>,
avoid_weekends: Option<bool>,
on_collition: Option<Move>,
}

View File

@ -1,159 +1,18 @@
use axum::extract::Query; mod handler;
use axum::http::header::CONTENT_TYPE; mod model;
use axum::response::{IntoResponse, Response}; mod service;
use axum::routing::get;
use axum::{Json, Router};
use chrono::{Datelike, Months, NaiveDate, NaiveDateTime, TimeDelta, Weekday};
use icalendar::{Calendar, Class, Component, Event, EventLike};
use serde::Deserialize;
use std::ops::Sub;
#[derive(Deserialize)] use axum::Router;
enum Recurring {
Daily,
Weeky,
BiWeekly,
Monthly,
Quarterly,
Yearly,
}
#[derive(Deserialize)]
enum Move {
Before,
After,
}
#[derive(Deserialize)]
struct EventQuery {
/// Start date
start_date_time: NaiveDateTime,
/// Recurring date
recurring: Recurring,
// If set, will ignore time on start_date
all_day: bool,
/// Title of the event
title: String,
// Optionals
end_date: Option<NaiveDate>,
avoid_weekends: Option<bool>,
on_collition: Option<Move>,
}
#[derive(Deserialize)]
enum Time {
/// Specific time at day
DateTime {
start: NaiveDateTime,
end: NaiveDateTime,
},
/// All day
Date(NaiveDate),
}
#[derive(Deserialize)]
struct EventBody {
/// Start date
time: Time,
/// Recurring date
recurring: Recurring,
/// Title of the event
title: String,
// Optionals
end_date: Option<NaiveDate>,
ignore_weekdays: Vec<Weekday>,
on_collition: Option<Move>,
}
/// TODO create ical object
/// TODO implement Before / After
/// TODO what if start_date is weekend
async fn get_calendar(Query(query): Query<EventQuery>) -> impl IntoResponse {
let start = query.start_date_time;
let end = query
.end_date
.unwrap_or_else(|| (start + TimeDelta::days(365)).date());
Json(get_dates(
start,
end,
query.recurring,
query.avoid_weekends.unwrap_or(false),
))
}
const TEXT_CALENDAR: &'static str = "text/calendar";
async fn get_ics(Query(query): Query<EventQuery>) -> impl IntoResponse {
let start = query.start_date_time;
let end = query
.end_date
.unwrap_or_else(|| (start + TimeDelta::days(365)).date());
let mut calendar = Calendar::new();
calendar.name(&query.title);
get_dates(
start,
end,
query.recurring,
query.avoid_weekends.unwrap_or(false),
)
.into_iter()
.for_each(|date| {
calendar.push(
Event::new()
.summary(&query.title)
.starts(date)
.class(Class::Confidential),
);
});
Response::builder()
.header(CONTENT_TYPE, TEXT_CALENDAR)
.body(calendar.done().to_string())
.unwrap()
}
fn get_dates(
start: NaiveDateTime,
end: NaiveDate,
recurring: Recurring,
avoid_weekends: bool,
) -> Vec<NaiveDateTime> {
let mut events = vec![start];
let mut previous_date_time = start;
let mut baseline = start;
while previous_date_time.date() < end {
let next = match recurring {
Recurring::Daily => todo!(),
Recurring::Weeky => todo!(),
Recurring::BiWeekly => todo!(),
Recurring::Monthly => {
let new_date = baseline.checked_add_months(Months::new(1)).unwrap();
baseline = new_date;
if let true = avoid_weekends {
match new_date.weekday() {
Weekday::Sat => new_date.sub(TimeDelta::days(1)),
Weekday::Sun => new_date.sub(TimeDelta::days(2)),
_ => new_date,
}
} else {
new_date
}
}
Recurring::Quarterly => todo!(),
Recurring::Yearly => todo!(),
};
events.push(next);
previous_date_time = next;
}
events
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let app = Router::new() const PORT: &'static str = "8000";
.route("/", get(get_calendar)) let app = Router::new().merge(handler::router());
.route("/ics", get(get_ics));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap(); println!("Starting Application on port {PORT}");
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{PORT}"))
.await
.unwrap();
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }

1
src/model/mod.rs Normal file
View File

@ -0,0 +1 @@
pub(crate) mod models;

17
src/model/models.rs Normal file
View File

@ -0,0 +1,17 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub enum Recurring {
Daily,
Weeky,
BiWeekly,
Monthly,
Quarterly,
Yearly,
}
#[derive(Deserialize)]
pub enum Move {
Before,
After,
}

View File

@ -0,0 +1,39 @@
use crate::model::models::Recurring;
use chrono::{Datelike, Months, NaiveDate, NaiveDateTime, TimeDelta, Weekday};
/// TODO implement Before / After
/// TODO what if start_date is weekend
pub fn get_dates(
start: NaiveDateTime,
end: NaiveDate,
recurring: Recurring,
avoid_weekends: bool,
) -> Vec<NaiveDateTime> {
let mut events = vec![start];
let mut previous_date_time = start;
let mut baseline = start;
while previous_date_time.date() < end {
let next = match recurring {
Recurring::Daily => todo!(),
Recurring::Weeky => todo!(),
Recurring::BiWeekly => todo!(),
Recurring::Monthly => {
baseline = baseline.checked_add_months(Months::new(1)).unwrap();
if let true = avoid_weekends {
match baseline.weekday() {
Weekday::Sat => baseline - TimeDelta::days(1),
Weekday::Sun => baseline - TimeDelta::days(2),
_ => baseline,
}
} else {
baseline
}
}
Recurring::Quarterly => todo!(),
Recurring::Yearly => todo!(),
};
events.push(next);
previous_date_time = next;
}
events
}

1
src/service/mod.rs Normal file
View File

@ -0,0 +1 @@
pub(crate) mod date_service;