Compare commits
18 Commits
e135a34d4c
...
f84c8226a7
| Author | SHA1 | Date | |
|---|---|---|---|
| f84c8226a7 | |||
| 024a212fe2 | |||
| 5a818bae56 | |||
| a4457dc2d7 | |||
| d1d7f89dfb | |||
| a65f9852f3 | |||
| 76067d76ac | |||
| 37b8fe5e56 | |||
| cf67c980a2 | |||
| 0ad039a597 | |||
| 3db69454e4 | |||
| a7947a31bc | |||
| d8416f3c99 | |||
| 8ed9d872dc | |||
| a86a28fd85 | |||
| 05ba925d54 | |||
| f7e55536ab | |||
| a398911527 |
1910
Cargo.lock
generated
1910
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@@ -10,7 +10,11 @@ chrono = { version = "0.4.42", features = ["serde"] }
|
||||
axum = { version = "0.8.8", features = ["macros"] }
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
actix-web = "4.12.1"
|
||||
paperclip = { version = "0.9.5", features = ["actix4"] }
|
||||
apistos = { version = "0.6.0", features = ["swagger-ui"] }
|
||||
schemars = { package = "apistos-schemars", version = "0.8" }
|
||||
sea-orm = { version = "1.1.19", features = [
|
||||
"macros",
|
||||
"runtime-tokio",
|
||||
"sqlx-sqlite",
|
||||
] }
|
||||
utoipa = { version = "5.4.0", features = ["axum_extras", "chrono"] }
|
||||
utoipa-axum = "0.2.0"
|
||||
utoipa-scalar = { version = "0.3.0", features = ["axum"] }
|
||||
|
||||
32
flake.lock
generated
32
flake.lock
generated
@@ -6,11 +6,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1763880175,
|
||||
"narHash": "sha256-WfItZn6duisxCxyltbu7Hs7kxzNeylgZGOwCYwHe26g=",
|
||||
"lastModified": 1770275419,
|
||||
"narHash": "sha256-g2wfAevB/IFF6Y1C74TbhRERlUVFVGQsgGp/lLR4lQM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "a563f057979806c59da53070297502eb7af22f62",
|
||||
"rev": "aa3fbaab2bdc73c5c1e25a124c272dde295bc957",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -47,11 +47,11 @@
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1763384566,
|
||||
"narHash": "sha256-r+wgI+WvNaSdxQmqaM58lVNvJYJ16zoq+tKN20cLst4=",
|
||||
"lastModified": 1769799857,
|
||||
"narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "d4155d6ebb70fbe2314959842f744aa7cabbbf6a",
|
||||
"rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -63,11 +63,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1763678758,
|
||||
"narHash": "sha256-+hBiJ+kG5IoffUOdlANKFflTT5nO3FrrR2CA3178Y5s=",
|
||||
"lastModified": 1770197578,
|
||||
"narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "117cc7f94e8072499b0a7aa4c52084fa4e11cc9b",
|
||||
"rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -95,16 +95,16 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1688392541,
|
||||
"narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=",
|
||||
"lastModified": 1770197578,
|
||||
"narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b",
|
||||
"rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-22.11",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
@@ -119,11 +119,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1763846202,
|
||||
"narHash": "sha256-f5PvQONttEQCjnQ52zAEVJvXDZ5l2gbItLfDyfcyGgk=",
|
||||
"lastModified": 1770200365,
|
||||
"narHash": "sha256-Z3V5v8tSwZ3l4COVSt0b6Av0wZwTUf7Qj0SQ2/Z5RX0=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "50621856a594a357c3aff0c5176ba8db4118133d",
|
||||
"rev": "1433910d1ffaff2c7a5fb7ba701f82ea578a99e3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
11
flake.nix
11
flake.nix
@@ -2,7 +2,7 @@
|
||||
inputs = {
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
naersk.url = "github:nix-community/naersk/master";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
@@ -101,12 +101,14 @@
|
||||
|
||||
in
|
||||
{
|
||||
shell = eachSystem (buildTargets) (
|
||||
devShells = eachSystem (builtins.attrNames buildTargets) (
|
||||
system:
|
||||
let
|
||||
pkgs = mkPkgs system null;
|
||||
in
|
||||
pkgs.mkShell {
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
name = "snooze-pal";
|
||||
buildInputs = with pkgs; [
|
||||
gcc
|
||||
openssl.dev
|
||||
@@ -115,9 +117,12 @@
|
||||
rustc
|
||||
cargo
|
||||
sea-orm-cli
|
||||
sqlite
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
packages = eachCrossSystem (builtins.attrNames buildTargets) (
|
||||
buildSystem: targetSystem:
|
||||
let
|
||||
|
||||
2
migration/.gitignore
vendored
Normal file
2
migration/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
result
|
||||
2129
migration/Cargo.lock
generated
Normal file
2129
migration/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
migration/Cargo.toml
Normal file
23
migration/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "migration"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "migration"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
tokio = {version = "1.49.0", features = ["macros", "rt-multi-thread"]}
|
||||
|
||||
[dependencies.sea-orm-migration]
|
||||
version = "1.1.0"
|
||||
features = [
|
||||
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
|
||||
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
|
||||
# e.g.
|
||||
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
|
||||
# "sqlx-postgres", # `DATABASE_DRIVER` feature
|
||||
"sqlx-sqlite"
|
||||
]
|
||||
41
migration/README.md
Normal file
41
migration/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Running Migrator CLI
|
||||
|
||||
- Generate a new migration file
|
||||
```sh
|
||||
cargo run -- generate MIGRATION_NAME
|
||||
```
|
||||
- Apply all pending migrations
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
```sh
|
||||
cargo run -- up
|
||||
```
|
||||
- Apply first 10 pending migrations
|
||||
```sh
|
||||
cargo run -- up -n 10
|
||||
```
|
||||
- Rollback last applied migrations
|
||||
```sh
|
||||
cargo run -- down
|
||||
```
|
||||
- Rollback last 10 applied migrations
|
||||
```sh
|
||||
cargo run -- down -n 10
|
||||
```
|
||||
- Drop all tables from the database, then reapply all migrations
|
||||
```sh
|
||||
cargo run -- fresh
|
||||
```
|
||||
- Rollback all applied migrations, then reapply all migrations
|
||||
```sh
|
||||
cargo run -- refresh
|
||||
```
|
||||
- Rollback all applied migrations
|
||||
```sh
|
||||
cargo run -- reset
|
||||
```
|
||||
- Check the status of all migrations
|
||||
```sh
|
||||
cargo run -- status
|
||||
```
|
||||
12
migration/src/lib.rs
Normal file
12
migration/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20220101_000001_create_table;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![Box::new(m20220101_000001_create_table::Migration)]
|
||||
}
|
||||
}
|
||||
36
migration/src/m20220101_000001_create_table.rs
Normal file
36
migration/src/m20220101_000001_create_table.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use sea_orm_migration::{prelude::*, schema::*};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// Replace the sample below with your own migration scripts
|
||||
|
||||
manager.create_table(
|
||||
Table::create()
|
||||
.table(Alarm::Table)
|
||||
.if_not_exists()
|
||||
.col(pk_auto(Alarm::Id))
|
||||
.col(date_time(Alarm::Time))
|
||||
.col(boolean(Alarm::Enabled))
|
||||
.col(string(Alarm::Title))
|
||||
.to_owned(),
|
||||
).await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// Replace the sample below with your own migration scripts
|
||||
manager.drop_table(Table::drop().table(Alarm::Table).to_owned()).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Alarm {
|
||||
Table,
|
||||
Id,
|
||||
Time,
|
||||
Enabled,
|
||||
Title
|
||||
}
|
||||
6
migration/src/main.rs
Normal file
6
migration/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
cli::run_cli(migration::Migrator).await;
|
||||
}
|
||||
BIN
snooze-pal.db
Normal file
BIN
snooze-pal.db
Normal file
Binary file not shown.
45
src/dao/alarm.rs
Normal file
45
src/dao/alarm.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::i32;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::Set, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, QueryFilter
|
||||
};
|
||||
|
||||
use crate::model::{self, alarm};
|
||||
|
||||
pub async fn create_alarm<C: ConnectionTrait>(
|
||||
db: &C,
|
||||
title: String,
|
||||
time: DateTime<Utc>,
|
||||
) -> Result<alarm::Model, DbErr> {
|
||||
let alarm_to_create = model::alarm::ActiveModel {
|
||||
title: Set(title),
|
||||
time: Set(time.naive_utc()),
|
||||
enabled: Set(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(alarm_to_create.insert(db).await?)
|
||||
}
|
||||
|
||||
pub async fn get_alarms<C: ConnectionTrait>(db: &C, enabled: Option<bool>) -> Result<Vec<alarm::Model>, DbErr> {
|
||||
let query = model::alarm::Entity::find();
|
||||
|
||||
let query = match enabled {
|
||||
Some(enabled) => {
|
||||
query.filter(model::alarm::Column::Enabled.eq(enabled))
|
||||
},
|
||||
None => {
|
||||
query
|
||||
}
|
||||
};
|
||||
|
||||
query.all(db).await
|
||||
}
|
||||
|
||||
pub async fn get_alarm<C: ConnectionTrait>(db: &C, id: i32) -> Result<Option<model::alarm::Model>, DbErr> {
|
||||
let result = model::alarm::Entity::find_by_id(id)
|
||||
.one(db).await;
|
||||
|
||||
return result
|
||||
}
|
||||
2
src/dao/mod.rs
Normal file
2
src/dao/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod alarm;
|
||||
|
||||
77
src/main.rs
77
src/main.rs
@@ -1,64 +1,81 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use apistos::{SwaggerUIConfig, app::{BuildConfig, OpenApiWrapper}, info::Info, spec::Spec};
|
||||
use cron_tab::Cron;
|
||||
use gpio_cdev::Chip;
|
||||
use sea_orm::Database;
|
||||
use tokio::net::TcpListener;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_axum::router::OpenApiRouter;
|
||||
use utoipa_scalar::{Scalar, Servable};
|
||||
|
||||
use crate::{ringer::BeepRinger, scheduler::Scheduler};
|
||||
use crate::{ringer::{BeepRinger, Ringer, SilentRinger}, scheduler::Scheduler};
|
||||
|
||||
mod scheduler;
|
||||
mod ringer;
|
||||
mod types;
|
||||
mod resources;
|
||||
mod dao;
|
||||
mod model;
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
scheduler: Arc<Scheduler<BeepRinger>>
|
||||
scheduler: Arc<Scheduler>
|
||||
}
|
||||
|
||||
fn app_state() -> AppState {
|
||||
let chip: Arc<Mutex<Chip>> = Arc::new(Mutex::new(Chip::new("/dev/gpiochip0").unwrap()));
|
||||
fn construct_ringer() -> Arc<Mutex<dyn Ringer>> {
|
||||
let result = Chip::new("/dev/gpiochip0");
|
||||
|
||||
match result {
|
||||
Ok(chip) => {
|
||||
let chip = Arc::new(Mutex::new(chip));
|
||||
Arc::new(Mutex::new(BeepRinger::new(chip))) as Arc<Mutex<dyn Ringer>>
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error opening chip (falling back to silent ringer): {}", e);
|
||||
Arc::new(Mutex::new(SilentRinger::new())) as Arc<Mutex<dyn Ringer>>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn app_state() -> AppState {
|
||||
let cron = Arc::new(Mutex::new(Cron::new(chrono::Local)));
|
||||
let db = Database::connect("sqlite://snooze-pal.db").await.unwrap();
|
||||
let ringer = construct_ringer();
|
||||
|
||||
AppState {
|
||||
scheduler: Arc::new(Scheduler::new(
|
||||
Arc::new(Mutex::new(BeepRinger::new(chip.clone()))),
|
||||
cron.clone()
|
||||
ringer,
|
||||
cron.clone(),
|
||||
Arc::new(db)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app_state = app_state();
|
||||
|
||||
app_state.scheduler.start();
|
||||
|
||||
let _ = start_actix_server(app_state).await;
|
||||
let app_state = app_state().await;
|
||||
app_state.scheduler.start().await;
|
||||
start_axum_server(app_state).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(OpenApi)]
|
||||
struct ApiDocs;
|
||||
|
||||
async fn start_actix_server(app_state: AppState) -> std::io::Result<()> {
|
||||
let _ = HttpServer::new(move || {
|
||||
let spec = Spec {
|
||||
info: Info {
|
||||
title: "Snooze Pal".to_string(),
|
||||
version: "0.0.1".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
async fn start_axum_server(app_state: AppState) {
|
||||
let docs = ApiDocs::openapi();
|
||||
|
||||
App::new()
|
||||
.document(spec)
|
||||
.service(resources::v1())
|
||||
.app_data(web::Data::new(app_state.clone()))
|
||||
.build_with("/openapi.json", BuildConfig::default().with(SwaggerUIConfig::new(&"/swagger")))
|
||||
let (router, spec) = OpenApiRouter::<AppState>::with_openapi(docs)
|
||||
.nest("/v1", resources::router())
|
||||
.with_state(app_state)
|
||||
.split_for_parts();
|
||||
|
||||
}).bind("0.0.0.0:8080")?.run().await;
|
||||
let router = router.merge(Scalar::with_url("/docs", spec));
|
||||
let listener = TcpListener::bind("0.0.0.0:8080")
|
||||
.await.expect("Failed to bind to port 8080. It may be taken by another process.");
|
||||
|
||||
Ok(())
|
||||
axum::serve(listener, router)
|
||||
.await.expect("Failed to serve");
|
||||
}
|
||||
|
||||
18
src/model/alarm.rs
Normal file
18
src/model/alarm.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "alarm")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub time: DateTime,
|
||||
pub enabled: bool,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
5
src/model/mod.rs
Normal file
5
src/model/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod alarm;
|
||||
1
src/model/prelude.rs
Normal file
1
src/model/prelude.rs
Normal file
@@ -0,0 +1 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
65
src/resources/alarm/get.rs
Normal file
65
src/resources/alarm/get.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use std::i32;
|
||||
|
||||
use axum::{Json, debug_handler, extract::{Query, State}, http::StatusCode, response::IntoResponse};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, IntoResponses, ToSchema};
|
||||
|
||||
use crate::{AppState, types::Alarm};
|
||||
|
||||
#[derive(ToSchema, Serialize)]
|
||||
pub struct OkResponse {
|
||||
id: i32,
|
||||
title: String,
|
||||
enabled: bool,
|
||||
time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(IntoResponses)]
|
||||
pub enum Responses {
|
||||
#[response(status = 200)]
|
||||
Ok(#[to_schema] Vec<OkResponse>),
|
||||
#[response(status = 500)]
|
||||
DBError(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for Responses {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
Responses::Ok(body) => (StatusCode::OK, Json(body)).into_response(),
|
||||
Responses::DBError(message) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(message)).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoParams, Deserialize)]
|
||||
pub struct RequestQuery {
|
||||
enabled: Option<bool>
|
||||
}
|
||||
|
||||
impl From<Alarm> for OkResponse {
|
||||
fn from(value: Alarm) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
title: value.title,
|
||||
enabled: value.enabled,
|
||||
time: value.time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "", responses(Responses), params(RequestQuery))]
|
||||
#[debug_handler]
|
||||
pub async fn get_handler(State(AppState{ scheduler }): State<AppState>, Query(RequestQuery { enabled }): Query<RequestQuery>) -> Responses {
|
||||
let result = scheduler.get_alarms(enabled).await;
|
||||
match result {
|
||||
Ok(alarms) => {
|
||||
Responses::Ok(alarms.into_iter().map(|alarm| alarm.into()).collect())
|
||||
},
|
||||
Err(error) => {
|
||||
Responses::DBError(error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
mod get;
|
||||
mod patch;
|
||||
mod post;
|
||||
use apistos::web;
|
||||
|
||||
pub fn resource() -> web::Resource {
|
||||
web::resource("/alarm")
|
||||
.route(web::post().to(post::post))
|
||||
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub fn router() -> OpenApiRouter<AppState> {
|
||||
OpenApiRouter::new().routes(routes!(
|
||||
post::post_handler,
|
||||
get::get_handler,
|
||||
patch::patch_handler
|
||||
))
|
||||
}
|
||||
|
||||
73
src/resources/alarm/patch.rs
Normal file
73
src/resources/alarm/patch.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use axum::Json;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{debug_handler, response::IntoResponse};
|
||||
use axum::extract::{Path, State};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoResponses, ToSchema};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(ToSchema, Serialize, Deserialize)]
|
||||
pub struct OkResponse {
|
||||
id: i32,
|
||||
title: String,
|
||||
enabled: bool,
|
||||
time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Deserialize)]
|
||||
pub struct PatchRequestBody {
|
||||
title: Option<String>,
|
||||
enabled: Option<bool>,
|
||||
time: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(IntoResponses)]
|
||||
pub enum Responses {
|
||||
#[response(status = 200)]
|
||||
Ok(#[to_schema] OkResponse),
|
||||
#[response(status = 500, description = "Something failed in the Database when trying to update the alarm")]
|
||||
DbError(String),
|
||||
#[response(status = 404, description = "The alarm you want to update was not found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl IntoResponse for Responses {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
Responses::Ok(body) => (StatusCode::OK, Json(body)).into_response(),
|
||||
Responses::DbError(message) => (StatusCode::INTERNAL_SERVER_ERROR, Json(message)).into_response(),
|
||||
Responses::NotFound => (StatusCode::NOT_FOUND).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::types::Alarm> for OkResponse {
|
||||
fn from(value: crate::types::Alarm) -> Self {
|
||||
OkResponse {
|
||||
id: value.id,
|
||||
title: value.title,
|
||||
enabled: value.enabled,
|
||||
time: value.time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(patch, path = "/{id}", responses(Responses))]
|
||||
#[debug_handler]
|
||||
pub async fn patch_handler(State(AppState { scheduler }): State<AppState>, Path(id): Path<i32>, Json(body): Json<PatchRequestBody>) -> Responses {
|
||||
let result = scheduler.update_alarm(id, body.title, body.enabled, body.time).await;
|
||||
|
||||
match result {
|
||||
Ok(Some(alarm)) => {
|
||||
Responses::Ok(alarm.into())
|
||||
},
|
||||
Ok(None) => {
|
||||
Responses::NotFound
|
||||
},
|
||||
Err(error) => {
|
||||
Responses::DbError(error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,75 @@
|
||||
use std::{fmt::Display};
|
||||
|
||||
use actix_web::{ResponseError, web::{Data, Json}};
|
||||
use apistos::{ApiComponent, ApiErrorComponent, actix::CreatedJson};
|
||||
use chrono::{DateTime, Local};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use axum::extract::State;
|
||||
use axum::{Json};
|
||||
use axum::http::StatusCode;
|
||||
use axum::debug_handler;
|
||||
use axum::response::IntoResponse;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoResponses, ToSchema};
|
||||
|
||||
use crate::AppState;
|
||||
use crate::types::Alarm;
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema, ApiComponent)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct RequestBody {
|
||||
time: DateTime<Local>,
|
||||
title: String,
|
||||
time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema, ApiComponent)]
|
||||
pub struct SuccessResponse {
|
||||
pub time: DateTime<Local>,
|
||||
pub enabled: bool
|
||||
#[derive(ToSchema, Serialize)]
|
||||
pub struct OkResponseBody {
|
||||
id: i32,
|
||||
time: DateTime<Utc>,
|
||||
title: String,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, ApiErrorComponent)]
|
||||
#[openapi_error(status(code = 500, description = "The alarm could not be created"))]
|
||||
pub enum ErrorResponse {
|
||||
InternalError(String)
|
||||
#[derive(IntoResponses)]
|
||||
pub enum Responses {
|
||||
#[response(status = 200)]
|
||||
Ok(#[to_schema] OkResponseBody),
|
||||
#[response(status = 500)]
|
||||
Error(String)
|
||||
}
|
||||
|
||||
impl Display for ErrorResponse {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl IntoResponse for Responses {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
ErrorResponse::InternalError(e) => write!(f, "Internal error: {}", e),
|
||||
Responses::Ok(body) => (StatusCode::OK, Json(body)).into_response(),
|
||||
Responses::Error(message) => (StatusCode::INTERNAL_SERVER_ERROR, Json(message)).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl From<Alarm> for OkResponseBody {
|
||||
fn from(value: Alarm) -> Self {
|
||||
OkResponseBody {
|
||||
time: value.time,
|
||||
enabled: value.enabled,
|
||||
title: value.title,
|
||||
id: value.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for ErrorResponse {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
match self {
|
||||
ErrorResponse::InternalError(_) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[apistos::api_operation(
|
||||
summary = "Add new alarm",
|
||||
description = r###"Creates new Alarm"###,
|
||||
error_code= 500,
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "",
|
||||
responses(
|
||||
Responses
|
||||
)
|
||||
)]
|
||||
pub async fn post(data: Data<AppState>, body: Json<RequestBody>) -> Result<CreatedJson<SuccessResponse>, ErrorResponse> {
|
||||
let result = data.scheduler.add_alarm(body.time);
|
||||
#[debug_handler]
|
||||
pub async fn post_handler(State(AppState { scheduler, ..}): State<AppState>, Json(body): Json<RequestBody>) -> Responses {
|
||||
let result = scheduler.add_alarm(body.time, body.title).await;
|
||||
|
||||
match result {
|
||||
Ok(alarm) => Ok(CreatedJson(SuccessResponse { time: alarm.time, enabled: alarm.enabled })),
|
||||
Err(e) => Err(ErrorResponse::InternalError(e)),
|
||||
Ok(alarm) => {
|
||||
Responses::Ok(alarm.into())
|
||||
},
|
||||
Err(e) => {
|
||||
Responses::Error(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use utoipa_axum::router::OpenApiRouter;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
mod alarm;
|
||||
|
||||
pub fn v1() -> apistos::web::Scope {
|
||||
apistos::web::scope("/v1").service(alarm::resource())
|
||||
pub fn router() -> OpenApiRouter<AppState> {
|
||||
OpenApiRouter::new().nest("/alarms", alarm::router())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use std::{sync::{Arc, Mutex}, thread};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
use gpio_cdev::{Chip, LineRequestFlags};
|
||||
|
||||
pub trait Ringer: Send + Sync {
|
||||
pub trait Ringer: Send + Sync + Debug {
|
||||
fn ring(&self) -> Result<(), String>;
|
||||
}
|
||||
|
||||
@@ -12,7 +16,6 @@ pub struct BeepRinger {
|
||||
}
|
||||
|
||||
impl BeepRinger {
|
||||
|
||||
fn beep(times: u32, chip: &mut Chip) -> Result<(), String> {
|
||||
let beeper = chip.get_line(17);
|
||||
|
||||
@@ -24,9 +27,7 @@ impl BeepRinger {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let beeper = beeper
|
||||
.request(LineRequestFlags::OUTPUT, 0, "my-gpio");
|
||||
let beeper = beeper.request(LineRequestFlags::OUTPUT, 0, "my-gpio");
|
||||
|
||||
let beeper = match beeper {
|
||||
Ok(beeper) => beeper,
|
||||
@@ -68,3 +69,21 @@ impl Ringer for BeepRinger {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for local testing without an actual beeper or similar. The only thing it does is print
|
||||
/// that it's ringing.
|
||||
#[derive(Debug)]
|
||||
pub struct SilentRinger;
|
||||
|
||||
impl SilentRinger {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ringer for SilentRinger {
|
||||
fn ring(&self) -> Result<(), String> {
|
||||
println!("Ringing");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
141
src/scheduler.rs
141
src/scheduler.rs
@@ -1,29 +1,40 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{i32, usize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use chrono::{DateTime, Local, Timelike};
|
||||
use chrono::{DateTime, Local, Timelike, Utc};
|
||||
use cron_tab::Cron;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, DbErr};
|
||||
|
||||
use crate::dao::alarm::{self, create_alarm, get_alarm, get_alarms};
|
||||
use crate::model;
|
||||
use crate::ringer::Ringer;
|
||||
use crate::types::Alarm;
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Scheduler<T: Ringer + 'static> {
|
||||
ringer: Arc<Mutex<T>>,
|
||||
pub struct Scheduler {
|
||||
ringer: Arc<Mutex<dyn Ringer>>,
|
||||
cron: Arc<Mutex<Cron<Local>>>,
|
||||
alarms: Arc<Mutex<Vec<Alarm>>>,
|
||||
db: Arc<DatabaseConnection>,
|
||||
cron_jobs: Arc<Mutex<HashMap<i32, usize>>>
|
||||
}
|
||||
|
||||
impl<T: Ringer> Scheduler<T> {
|
||||
pub fn new(ringer: Arc<Mutex<T>>, cron: Arc<Mutex<Cron<Local>>>) -> Self {
|
||||
impl Scheduler {
|
||||
pub fn new(
|
||||
ringer: Arc<Mutex<dyn Ringer>>,
|
||||
cron: Arc<Mutex<Cron<Local>>>,
|
||||
db: Arc<DatabaseConnection>,
|
||||
) -> Self {
|
||||
Self {
|
||||
ringer,
|
||||
cron,
|
||||
alarms: Arc::new(Mutex::new(Vec::new())),
|
||||
db,
|
||||
cron_jobs: Arc::new(Mutex::new(HashMap::new()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn schedule(&self, cron_schedule: &str) -> Result<(), String> {
|
||||
pub fn schedule(&self, cron_schedule: &str) -> Result<usize, String> {
|
||||
let ringer = self.ringer.clone();
|
||||
let cron = self.cron.clone();
|
||||
|
||||
@@ -37,22 +48,114 @@ impl<T: Ringer> Scheduler<T> {
|
||||
}
|
||||
});
|
||||
|
||||
job_result.expect("Faild to add job");
|
||||
todo!()
|
||||
|
||||
Ok(job_result.expect("Faild to add job"))
|
||||
}
|
||||
|
||||
pub fn add_alarm(&self, time: DateTime<Local>) -> Result<Alarm, String> {
|
||||
let mut alarms = self.alarms.lock().map_err(|e| e.to_string())?;
|
||||
let cron_schedule = format!("{} {} {} * * *", "*", time.minute(), time.hour());
|
||||
let alarm = Alarm::new(true, time);
|
||||
alarms.push(alarm.clone());
|
||||
self.schedule(&cron_schedule).map_err(|e| e.to_string())?;
|
||||
pub async fn add_alarm(&self, time: DateTime<Utc>, title: String) -> Result<Alarm, String> {
|
||||
let cron_schedule = self.construct_cron_schedule(time);
|
||||
|
||||
let created_alarm = create_alarm(&*self.db, title, time)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let job_id = self.schedule(&cron_schedule).map_err(|e| e.to_string())?;
|
||||
self.cron_jobs.lock().expect("Failed to lock cron_jobs").insert(created_alarm.id, job_id);
|
||||
println!("Added alarm {}", cron_schedule);
|
||||
|
||||
Ok(alarm)
|
||||
Ok(created_alarm.into())
|
||||
}
|
||||
|
||||
pub fn start(&self) {
|
||||
pub async fn get_alarms(&self, enabled: Option<bool>) -> Result<Vec<Alarm>, DbErr> {
|
||||
get_alarms(&*self.db, enabled).await
|
||||
.map(|alarms| alarms
|
||||
.into_iter()
|
||||
.map(|a| a.into())
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn update_alarm(&self, id: i32, title: Option<String>, enabled: Option<bool>, time: Option<DateTime<Utc>>) -> Result<Option<Alarm>, DbErr> {
|
||||
let alarm = get_alarm(&*self.db, id).await?;
|
||||
|
||||
let Some(alarm) = alarm else {
|
||||
return Ok(None)
|
||||
};
|
||||
|
||||
let mut active_alarm: model::alarm::ActiveModel = alarm.into();
|
||||
|
||||
if let Some(title) = title {
|
||||
active_alarm.title = Set(title.to_owned());
|
||||
}
|
||||
|
||||
if let Some(enabled) = enabled {
|
||||
active_alarm.enabled = Set(enabled);
|
||||
}
|
||||
|
||||
if let Some(time) = time {
|
||||
active_alarm.time = Set(time.naive_utc());
|
||||
}
|
||||
|
||||
let updated_alarm = active_alarm.update(&*self.db).await?;
|
||||
|
||||
self.reregister_alarm(updated_alarm.clone().into());
|
||||
|
||||
return Ok(Some(updated_alarm.into()));
|
||||
}
|
||||
|
||||
pub async fn start(&self) {
|
||||
self.cron.lock().expect("Failed to lock cron").start();
|
||||
self.register_existing_alarms().await;
|
||||
}
|
||||
|
||||
async fn register_existing_alarms(&self) {
|
||||
let alarms_result = self.get_alarms(Some(true)).await;
|
||||
|
||||
match alarms_result {
|
||||
Ok(alarms) => {
|
||||
self.register_alarms(alarms);
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Failed to get alarms: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_alarms(&self, alarms: Vec<Alarm>) {
|
||||
alarms.into_iter().for_each(|a| {
|
||||
self.register_alarm(a);
|
||||
});
|
||||
}
|
||||
|
||||
fn register_alarm(&self, alarm: Alarm) {
|
||||
let cron_schedule = self.construct_cron_schedule(alarm.time);
|
||||
let scheduling_result = self.schedule(&cron_schedule);
|
||||
|
||||
match scheduling_result {
|
||||
Ok(job_id) => {
|
||||
self.cron_jobs.lock().expect("Failed to lock cron_jobs").insert(alarm.id, job_id);
|
||||
println!("Registered alarm {}", cron_schedule);
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Failed to register alarm {}: {}", cron_schedule, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn construct_cron_schedule(&self, time: DateTime<Utc>) -> String {
|
||||
let time = time.with_timezone(&Local);
|
||||
format!("{} {} {} * * *", "*", time.minute(), time.hour())
|
||||
}
|
||||
|
||||
fn reregister_alarm(&self, alarm: Alarm) {
|
||||
let job_id = self.cron_jobs.lock().expect("Failed to lock cron_jobs")
|
||||
.remove(&alarm.id);
|
||||
|
||||
if let Some(job_id) = job_id {
|
||||
self.cron.lock().expect("Failed to lock cron handler").remove(job_id);
|
||||
}
|
||||
|
||||
let new_job_id = self.schedule(&self.construct_cron_schedule(alarm.time)).expect("failed to register alarm");
|
||||
self.cron_jobs.lock().expect("Failed to lock cron_jobs").insert(alarm.id, new_job_id);
|
||||
}
|
||||
}
|
||||
|
||||
24
src/types.rs
24
src/types.rs
@@ -1,16 +1,24 @@
|
||||
use apistos::ApiComponent;
|
||||
use chrono::{DateTime, Local};
|
||||
use schemars::JsonSchema;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, ApiComponent)]
|
||||
use crate::model;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Alarm {
|
||||
pub id: i32,
|
||||
pub enabled: bool,
|
||||
pub time: DateTime<Local>,
|
||||
pub time: DateTime<Utc>,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
impl Alarm {
|
||||
pub fn new(enabled: bool, time: DateTime<Local>) -> Self {
|
||||
Self { enabled, time }
|
||||
|
||||
impl From<model::alarm::Model> for Alarm {
|
||||
fn from(value: model::alarm::Model) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
enabled: value.enabled,
|
||||
time: DateTime::from_naive_utc_and_offset(value.time, Utc),
|
||||
title: value.title,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user