Add saving periods

This commit is contained in:
Tobias Reisinger 2021-11-05 16:32:30 +01:00
parent 12d57d020f
commit 483fd60daa
13 changed files with 224 additions and 49 deletions

1
.gitignore vendored
View file

@ -9,3 +9,4 @@ emgauwa-core.sqlite
# Added by cargo # Added by cargo
/target /target
/api.http

BIN
Cargo.lock generated

Binary file not shown.

View file

@ -11,6 +11,7 @@ authors = ["Tobias Reisinger <tobias@msrg.cc>"]
[dependencies] [dependencies]
actix-web = "3" actix-web = "3"
chrono = { version = "0.4", features = ["serde"] }
diesel = { version = "1.4", features = ["sqlite", "uuid"] } diesel = { version = "1.4", features = ["sqlite", "uuid"] }
diesel_migrations = "1.4" diesel_migrations = "1.4"
dotenv = "0.15" dotenv = "0.15"

View file

@ -41,13 +41,13 @@ CREATE TABLE schedules
UNIQUE, UNIQUE,
name VARCHAR(128) name VARCHAR(128)
NOT NULL, NOT NULL,
periods TEXT periods BLOB
NOT NULL NOT NULL
); );
--INSERT INTO schedules (uid, name, periods) VALUES (x'6f666600000000000000000000000000', 'off', x'00'); --INSERT INTO schedules (uid, name, periods) VALUES (x'6f666600000000000000000000000000', 'off', x'00');
--INSERT INTO schedules (uid, name, periods) VALUES (x'6f6e0000000000000000000000000000', 'on', x'010000009F05'); --INSERT INTO schedules (uid, name, periods) VALUES (x'6f6e0000000000000000000000000000', 'on', x'010000009F05');
INSERT INTO schedules (uid, name, periods) VALUES (x'00', 'off', '00'); INSERT INTO schedules (uid, name, periods) VALUES (x'00', 'off', x'');
INSERT INTO schedules (uid, name, periods) VALUES (x'01', 'on', '010000009F05'); INSERT INTO schedules (uid, name, periods) VALUES (x'01', 'on', x'0000173B');
CREATE TABLE tags CREATE TABLE tags
( (

View file

@ -1,20 +1,18 @@
use std::env;
use diesel::dsl::sql;
use diesel::prelude::*;
use diesel_migrations::embed_migrations;
use dotenv::dotenv;
use errors::DatabaseError;
use models::*;
use schema::schedules::dsl::*;
use crate::types::EmgauwaUid;
pub mod errors; pub mod errors;
pub mod models; pub mod models;
pub mod schema; pub mod schema;
mod types;
use diesel::prelude::*;
use diesel::dsl::sql;
use dotenv::dotenv;
use std::env;
use models::*;
use schema::schedules::dsl::*;
use diesel_migrations::embed_migrations;
use errors::DatabaseError;
use types::EmgauwaUid;
embed_migrations!("migrations"); embed_migrations!("migrations");
@ -34,18 +32,27 @@ pub fn run_migrations() {
pub fn get_schedules() -> Vec<Schedule> { pub fn get_schedules() -> Vec<Schedule> {
let connection = get_connection(); let connection = get_connection();
schedules schedules
.limit(5)
.load::<Schedule>(&connection) .load::<Schedule>(&connection)
.expect("Error loading schedules") .expect("Error loading schedules")
} }
pub fn create_schedule(new_name: &str) -> Result<Schedule, DatabaseError> { pub fn get_schedule_by_uid(filter_uid: EmgauwaUid) -> Result<Schedule, DatabaseError> {
let connection = get_connection();
let result = schedules
.filter(uid.eq(filter_uid))
.first::<Schedule>(&connection)
.or(Err(DatabaseError::NotFound))?;
Ok(result)
}
pub fn create_schedule(new_name: &str, new_periods: &Periods) -> Result<Schedule, DatabaseError> {
let connection = get_connection(); let connection = get_connection();
let new_schedule = NewSchedule { let new_schedule = NewSchedule {
uid: &EmgauwaUid::default(), uid: &EmgauwaUid::default(),
name: new_name, name: new_name,
periods: "", periods: new_periods
}; };
diesel::insert_into(schedules) diesel::insert_into(schedules)

View file

@ -4,6 +4,16 @@ use serde::{Serialize, Serializer};
pub enum DatabaseError { pub enum DatabaseError {
InsertError, InsertError,
InsertGetError, InsertGetError,
NotFound,
}
impl DatabaseError {
fn to_code(&self) -> u32 {
match self {
DatabaseError::NotFound => 404,
_ => 500
}
}
} }
impl Serialize for DatabaseError { impl Serialize for DatabaseError {
@ -11,8 +21,9 @@ impl Serialize for DatabaseError {
where where
S: Serializer, S: Serializer,
{ {
let mut s = serializer.serialize_struct("error", 2)?; let mut s = serializer.serialize_struct("error", 3)?;
s.serialize_field("code", &500)?; s.serialize_field("type", "database-error")?;
s.serialize_field("code", &self.to_code())?;
s.serialize_field("description", &String::from(self))?; s.serialize_field("description", &String::from(self))?;
s.end() s.end()
} }
@ -25,6 +36,7 @@ impl From<&DatabaseError> for String {
DatabaseError::InsertGetError => { DatabaseError::InsertGetError => {
String::from("error retrieving new entry from database (your entry was saved)") String::from("error retrieving new entry from database (your entry was saved)")
} }
DatabaseError::NotFound => String::from("model was not found in database")
} }
} }
} }

View file

@ -1,14 +1,24 @@
use super::types::EmgauwaUid; use chrono::{NaiveTime, Timelike};
use serde::Serialize; use diesel::backend::Backend;
use diesel::deserialize::FromSql;
use diesel::serialize::{IsNull, Output, ToSql};
use diesel::sql_types::Binary;
use diesel::sqlite::Sqlite;
use diesel::{deserialize, serialize};
use serde::{Deserialize, Serialize};
use std::io::Write;
use super::schema::schedules; use super::schema::schedules;
use crate::types::EmgauwaUid;
#[derive(Serialize, Queryable)] #[derive(Serialize, Queryable)]
pub struct Schedule { pub struct Schedule {
#[serde(skip)]
pub id: i32, pub id: i32,
#[serde(alias = "id")]
pub uid: EmgauwaUid, pub uid: EmgauwaUid,
pub name: String, pub name: String,
pub periods: String, pub periods: Periods,
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -16,5 +26,74 @@ pub struct Schedule {
pub struct NewSchedule<'a> { pub struct NewSchedule<'a> {
pub uid: &'a EmgauwaUid, pub uid: &'a EmgauwaUid,
pub name: &'a str, pub name: &'a str,
pub periods: &'a str, pub periods: &'a Periods,
}
#[derive(Debug, Serialize, Deserialize, AsExpression, FromSqlRow, PartialEq, Clone)]
#[sql_type = "Binary"]
pub struct Period {
#[serde(with = "period_format")]
pub start: NaiveTime,
#[serde(with = "period_format")]
pub end: NaiveTime,
}
#[derive(Debug, Serialize, Deserialize, AsExpression, FromSqlRow, PartialEq, Clone)]
#[sql_type = "Binary"]
pub struct Periods(pub(crate) Vec<Period>);
mod period_format {
use chrono::NaiveTime;
use serde::{self, Deserialize, Deserializer, Serializer};
const FORMAT: &'static str = "%H:%M";
pub fn serialize<S>(time: &NaiveTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = format!("{}", time.format(FORMAT));
serializer.serialize_str(&s)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveTime, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
NaiveTime::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)
}
}
impl ToSql<Binary, Sqlite> for Periods {
fn to_sql<W: Write>(&self, out: &mut Output<W, Sqlite>) -> serialize::Result {
for period in self.0.iter() {
out.write_all(&[
period.start.hour() as u8,
period.start.minute() as u8,
period.end.hour() as u8,
period.end.minute() as u8,
])?;
}
Ok(IsNull::No)
}
}
impl FromSql<Binary, Sqlite> for Periods {
fn from_sql(bytes: Option<&<Sqlite as Backend>::RawValue>) -> deserialize::Result<Self> {
let blob = bytes.unwrap().read_blob();
let mut vec = Vec::new();
for i in (3..blob.len()).step_by(4) {
let start_val_h: u32 = blob[i - 3] as u32;
let start_val_m: u32 = blob[i - 2] as u32;
let end_val_h: u32 = blob[i - 1] as u32;
let end_val_m: u32 = blob[i - 0] as u32;
vec.push(Period {
start: NaiveTime::from_hms(start_val_h, start_val_m, 0),
end: NaiveTime::from_hms(end_val_h, end_val_m, 0),
});
}
Ok(Periods(vec))
}
} }

View file

@ -60,7 +60,7 @@ table! {
id -> Integer, id -> Integer,
uid -> Binary, uid -> Binary,
name -> Text, name -> Text,
periods -> Text, periods -> Binary,
} }
} }

34
src/handlers/errors.rs Normal file
View file

@ -0,0 +1,34 @@
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
pub enum HandlerError {
BadUidError,
}
impl HandlerError {
fn to_code(&self) -> u32 {
match self {
HandlerError::BadUidError => 400
}
}
}
impl Serialize for HandlerError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("error", 2)?;
s.serialize_field("code", &self.to_code())?;
s.serialize_field("description", &String::from(self))?;
s.end()
}
}
impl From<&HandlerError> for String {
fn from(err: &HandlerError) -> Self {
match err {
HandlerError::BadUidError => String::from("the uid is in a bad format"),
}
}
}

View file

@ -1 +1,2 @@
pub mod v1; pub mod v1;
mod errors;

View file

@ -1,17 +1,58 @@
use std::str::FromStr;
use actix_web::{HttpResponse, Responder, web, get};
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use crate::db; use crate::db;
use actix_web::{HttpResponse, Responder}; use crate::db::models::Periods;
use crate::handlers::errors::HandlerError;
use crate::types::EmgauwaUid;
#[derive(Debug, Serialize, Deserialize)]
pub struct RequestSchedule {
name: String,
periods: Periods,
}
pub async fn index() -> impl Responder { pub async fn index() -> impl Responder {
let schedules = db::get_schedules(); let schedules = db::get_schedules();
HttpResponse::Ok().json(schedules) HttpResponse::Ok().json(schedules)
} }
pub async fn get() -> impl Responder { #[get("/api/v1/schedules/{schedule_id}")]
"hello from get schedules by id" pub async fn show(web::Path((schedule_uid,)): web::Path<(String,)>) -> impl Responder {
let emgauwa_uid = match schedule_uid.as_str() {
"on" => Ok(EmgauwaUid::On),
"off" => Ok(EmgauwaUid::Off),
any => match Uuid::from_str(any) {
Ok(uuid) => Ok(EmgauwaUid::Any(uuid)),
Err(_) => Err(HandlerError::BadUidError)
}
};
match emgauwa_uid {
Ok(uid) => {
let schedule = db::get_schedule_by_uid(uid);
match schedule {
Ok(ok) => HttpResponse::Ok().json(ok),
Err(err) => HttpResponse::NotFound().json(err),
}
},
Err(err) => HttpResponse::BadRequest().json(err)
} }
pub async fn add() -> impl Responder { }
let new_schedule = db::create_schedule("TEST");
pub async fn add(post: web::Json<RequestSchedule>) -> impl Responder {
println!("model: {:?}", post);
for period in post.periods.0.iter() {
println!("start: {:?}; end: {:?}", period.start, period.end);
}
let new_schedule = db::create_schedule(&post.name, &post.periods);
match new_schedule { match new_schedule {
Ok(ok) => HttpResponse::Ok().json(ok), Ok(ok) => HttpResponse::Ok().json(ok),

View file

@ -1,13 +1,14 @@
mod db;
mod handlers;
#[macro_use] #[macro_use]
extern crate diesel; extern crate diesel;
#[macro_use] #[macro_use]
extern crate diesel_migrations; extern crate diesel_migrations;
extern crate dotenv; extern crate dotenv;
use actix_web::{web, App, HttpServer}; use actix_web::{App, HttpServer, web};
mod db;
mod handlers;
mod types;
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
@ -23,10 +24,7 @@ async fn main() -> std::io::Result<()> {
"/api/v1/schedules", "/api/v1/schedules",
web::post().to(handlers::v1::schedules::add), web::post().to(handlers::v1::schedules::add),
) )
.route( .service(handlers::v1::schedules::show)
"/api/v1/schedules/{id}",
web::get().to(handlers::v1::schedules::get),
)
.route( .route(
"/api/v1/schedules/{id}", "/api/v1/schedules/{id}",
web::delete().to(handlers::v1::schedules::delete), web::delete().to(handlers::v1::schedules::delete),

View file

@ -1,3 +1,6 @@
use std::fmt::{Debug, Formatter};
use std::io::Write;
use diesel::backend::Backend; use diesel::backend::Backend;
use diesel::deserialize::FromSql; use diesel::deserialize::FromSql;
use diesel::serialize::{IsNull, Output, ToSql}; use diesel::serialize::{IsNull, Output, ToSql};
@ -5,8 +8,6 @@ use diesel::sql_types::Binary;
use diesel::sqlite::Sqlite; use diesel::sqlite::Sqlite;
use diesel::{deserialize, serialize}; use diesel::{deserialize, serialize};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use std::fmt::{Debug, Formatter};
use std::io::Write;
use uuid::Uuid; use uuid::Uuid;
#[derive(AsExpression, FromSqlRow, PartialEq, Clone)] #[derive(AsExpression, FromSqlRow, PartialEq, Clone)]
@ -22,6 +23,7 @@ impl Default for EmgauwaUid {
EmgauwaUid::Any(Uuid::new_v4()) EmgauwaUid::Any(Uuid::new_v4())
} }
} }
impl Debug for EmgauwaUid { impl Debug for EmgauwaUid {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -31,6 +33,7 @@ impl Debug for EmgauwaUid {
} }
} }
} }
impl ToSql<Binary, Sqlite> for EmgauwaUid { impl ToSql<Binary, Sqlite> for EmgauwaUid {
fn to_sql<W: Write>(&self, out: &mut Output<W, Sqlite>) -> serialize::Result { fn to_sql<W: Write>(&self, out: &mut Output<W, Sqlite>) -> serialize::Result {
match self { match self {
@ -41,6 +44,7 @@ impl ToSql<Binary, Sqlite> for EmgauwaUid {
Ok(IsNull::No) Ok(IsNull::No)
} }
} }
impl FromSql<Binary, Sqlite> for EmgauwaUid { impl FromSql<Binary, Sqlite> for EmgauwaUid {
fn from_sql(bytes: Option<&<Sqlite as Backend>::RawValue>) -> deserialize::Result<Self> { fn from_sql(bytes: Option<&<Sqlite as Backend>::RawValue>) -> deserialize::Result<Self> {
match bytes { match bytes {
@ -59,13 +63,10 @@ impl Serialize for EmgauwaUid {
where where
S: Serializer, S: Serializer,
{ {
match self { String::from(self).serialize(serializer)
EmgauwaUid::On => "off".serialize(serializer),
EmgauwaUid::Off => "on".serialize(serializer),
EmgauwaUid::Any(value) => value.serialize(serializer),
}
} }
} }
impl From<Uuid> for EmgauwaUid { impl From<Uuid> for EmgauwaUid {
fn from(uid: Uuid) -> EmgauwaUid { fn from(uid: Uuid) -> EmgauwaUid {
match uid.as_u128() { match uid.as_u128() {
@ -89,8 +90,8 @@ impl From<&EmgauwaUid> for Uuid {
impl From<&EmgauwaUid> for String { impl From<&EmgauwaUid> for String {
fn from(emgauwa_uid: &EmgauwaUid) -> String { fn from(emgauwa_uid: &EmgauwaUid) -> String {
match emgauwa_uid { match emgauwa_uid {
EmgauwaUid::On => String::from("off"), EmgauwaUid::Off => String::from("off"),
EmgauwaUid::Off => String::from("on"), EmgauwaUid::On => String::from("on"),
EmgauwaUid::Any(value) => value.to_hyphenated().to_string(), EmgauwaUid::Any(value) => value.to_hyphenated().to_string(),
} }
} }