Compare commits
4 Commits
8ae429ac54
...
master
Author | SHA1 | Date | |
---|---|---|---|
477c74d5d2
|
|||
2326bb6e21
|
|||
9017888794
|
|||
a69b8a9c55
|
14
.gitea/workflows/deploy.yml
Normal file
14
.gitea/workflows/deploy.yml
Normal 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
|
20
.idea/runConfigurations/Run_recurring_event_api.xml
generated
Normal file
20
.idea/runConfigurations/Run_recurring_event_api.xml
generated
Normal 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
24
Cargo.lock
generated
@ -125,18 +125,6 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "caldendar-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
"icalendar",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.37"
|
||||
@ -537,6 +525,18 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "recurring-event-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
"icalendar",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
|
@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "caldendar-api"
|
||||
name = "recurring-event-api"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
@ -9,4 +9,4 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0" }
|
||||
icalendar = { version = "0.17", features = ["serde", "serde_json"] }
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||
|
27
Containerfile
Normal file
27
Containerfile
Normal 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
10
compose.yaml
Normal 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
46
src/handler/ics.rs
Normal 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
24
src/handler/index.rs
Normal 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
27
src/handler/mod.rs
Normal 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>,
|
||||
}
|
163
src/main.rs
163
src/main.rs
@ -1,159 +1,18 @@
|
||||
use axum::extract::Query;
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
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;
|
||||
mod handler;
|
||||
mod model;
|
||||
mod service;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
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
|
||||
}
|
||||
use axum::Router;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = Router::new()
|
||||
.route("/", get(get_calendar))
|
||||
.route("/ics", get(get_ics));
|
||||
const PORT: &'static str = "8000";
|
||||
let app = Router::new().merge(handler::router());
|
||||
|
||||
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();
|
||||
}
|
||||
|
1
src/model/mod.rs
Normal file
1
src/model/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub(crate) mod models;
|
17
src/model/models.rs
Normal file
17
src/model/models.rs
Normal 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,
|
||||
}
|
39
src/service/date_service.rs
Normal file
39
src/service/date_service.rs
Normal 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
1
src/service/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub(crate) mod date_service;
|
Reference in New Issue
Block a user