commit 12d57d020f5d7defceca5ffbbd981bd127848156 Author: Tobias Reisinger Date: Thu Nov 4 23:37:16 2021 +0100 Start rust rewrite diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f303022 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: +# https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.yml] +indent_size = 2 diff --git a/.env b/.env new file mode 100644 index 0000000..6075b6a --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=emgauwa-core.sqlite diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f575be --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +target/ + +tests/testing/ + +emgauwa-core.conf.d +emgauwa-core.sqlite + + +# Added by cargo + +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a9b3813 Binary files /dev/null and b/Cargo.lock differ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..760a6c4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "emgauwa-core" +version = "0.1.0" +edition = "2021" +authors = ["Tobias Reisinger "] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +#[profile.release] +#panic = 'abort' + +[dependencies] +actix-web = "3" +diesel = { version = "1.4", features = ["sqlite", "uuid"] } +diesel_migrations = "1.4" +dotenv = "0.15" +serde = "1.0" +serde_json = "1.0" +serde_derive = "1.0" +uuid = { version = "0.8", features = ["serde", "v4"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a43b28 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +[![Build Status](https://ci.serguzim.me/api/badges/emgauwa/core/status.svg)](https://ci.serguzim.me/emgauwa/core) \ No newline at end of file diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..71215db --- /dev/null +++ b/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/db/schema.rs" diff --git a/emgauwa-core.conf b/emgauwa-core.conf new file mode 100644 index 0000000..d433d74 --- /dev/null +++ b/emgauwa-core.conf @@ -0,0 +1,17 @@ +database = "emgauwa-core.sqlite" +content-dir = "/usr/share/webapps/emgauwa" + +[not-found] +file = "404.html" +content = "404 - NOT FOUND" +content-type = "text/plain" + +[bind] +http = "127.0.0.1:5000" +mqtt = "127.0.0.1:1883" + +[logging] +level = "debug" +file = "stdout" + +# vim: set ft=toml: diff --git a/migrations/2021-10-13-000000_init/down.sql b/migrations/2021-10-13-000000_init/down.sql new file mode 100644 index 0000000..2547da8 --- /dev/null +++ b/migrations/2021-10-13-000000_init/down.sql @@ -0,0 +1,8 @@ +DROP TABLE macro_actions; +DROP TABLE macros; +DROP TABLE junction_relay_schedule; +DROP TABLE junction_tag; +DROP TABLE tags; +DROP TABLE schedules; +DROP TABLE relays; +DROP TABLE controllers; diff --git a/migrations/2021-10-13-000000_init/up.sql b/migrations/2021-10-13-000000_init/up.sql new file mode 100644 index 0000000..0159598 --- /dev/null +++ b/migrations/2021-10-13-000000_init/up.sql @@ -0,0 +1,128 @@ +CREATE TABLE controllers +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT + NOT NULL, + uid VARCHAR(36) + NOT NULL + UNIQUE, + name VARCHAR(128), + ip VARCHAR(16), + port INTEGER, + relay_count INTEGER, + active BOOLEAN + NOT NULL +); + +CREATE TABLE relays +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT + NOT NULL, + name VARCHAR(128), + number INTEGER + NOT NULL, + controller_id INTEGER + NOT NULL + REFERENCES controllers (id) + ON DELETE CASCADE +); + +CREATE TABLE schedules +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT + NOT NULL, + uid BLOB + NOT NULL + UNIQUE, + name VARCHAR(128) + NOT NULL, + periods TEXT + NOT NULL +); +--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'00', 'off', '00'); +INSERT INTO schedules (uid, name, periods) VALUES (x'01', 'on', '010000009F05'); + +CREATE TABLE tags +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT + NOT NULL, + tag VARCHAR(128) + NOT NULL + UNIQUE +); + +CREATE TABLE junction_tag +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT + NOT NULL, + tag_id INTEGER + NOT NULL + REFERENCES tags (id) + ON DELETE CASCADE, + relay_id INTEGER + REFERENCES relays (id) + ON DELETE CASCADE, + schedule_id INTEGER + REFERENCES schedules (id) + ON DELETE CASCADE +); + +CREATE TABLE junction_relay_schedule +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT + NOT NULL, + weekday SMALLINT + NOT NULL, + relay_id INTEGER + REFERENCES relays (id) + ON DELETE CASCADE, + schedule_id INTEGER + DEFAULT 1 + REFERENCES schedules (id) + ON DELETE SET DEFAULT +); + +CREATE TABLE macros +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT + NOT NULL, + uid VARCHAR(36) + NOT NULL + UNIQUE, + name VARCHAR(128) +); + +CREATE TABLE macro_actions +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT + NOT NULL, + macro_id INTEGER + NOT NULL + REFERENCES macros (id) + ON DELETE CASCADE, + relay_id INTEGER + REFERENCES relays (id) + ON DELETE CASCADE, + schedule_id INTEGER + REFERENCES schedules (id) + ON DELETE CASCADE, + weekday SMALLINT + NOT NULL +); diff --git a/sql/cache.sql b/sql/cache.sql new file mode 100644 index 0000000..3e4ce21 --- /dev/null +++ b/sql/cache.sql @@ -0,0 +1,10 @@ +-- a key-value table used for the json-cache + +CREATE TABLE cache ( + key STRING + PRIMARY KEY, + value TEXT + NOT NULL, + expiration INT + DEFAULT 0 +); diff --git a/sql/migration_0.sql b/sql/migration_0.sql new file mode 100644 index 0000000..939b907 --- /dev/null +++ b/sql/migration_0.sql @@ -0,0 +1,83 @@ +-- base migration + +CREATE TABLE controllers +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT, + uid BLOB + NOT NULL + UNIQUE, + name VARCHAR(128), + ip VARCHAR(16), + port INTEGER, + relay_count INTEGER, + active BOOLEAN + NOT NULL +); + +CREATE TABLE relays +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT, + name VARCHAR(128), + number INTEGER + NOT NULL, + controller_id INTEGER + NOT NULL + REFERENCES controllers (id) + ON DELETE CASCADE +); + +CREATE TABLE schedules +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT, + uid BLOB + NOT NULL + UNIQUE, + name VARCHAR(128), + periods BLOB +); + +CREATE TABLE tags +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT, + tag VARCHAR(128) + NOT NULL + UNIQUE +); + +CREATE TABLE junction_tag +( + tag_id INTEGER + NOT NULL + REFERENCES tags (id) + ON DELETE CASCADE, + relay_id INTEGER + REFERENCES relays (id) + ON DELETE CASCADE, + schedule_id INTEGER + REFERENCES schedules (id) + ON DELETE CASCADE +); + +CREATE TABLE junction_relay_schedule +( + weekday SMALLINT + NOT NULL, + relay_id INTEGER + REFERENCES relays (id) + ON DELETE CASCADE, + schedule_id INTEGER + DEFAULT 1 + REFERENCES schedules (id) + ON DELETE SET DEFAULT +); + +INSERT INTO schedules (uid, name, periods) VALUES (x'6f666600000000000000000000000000', 'off', x'00'); +INSERT INTO schedules (uid, name, periods) VALUES (x'6f6e0000000000000000000000000000', 'on', x'010000009F05'); diff --git a/sql/migration_1.sql b/sql/migration_1.sql new file mode 100644 index 0000000..5078763 --- /dev/null +++ b/sql/migration_1.sql @@ -0,0 +1,28 @@ +-- migration to add macros + +CREATE TABLE macros +( + id INTEGER + PRIMARY KEY + AUTOINCREMENT, + uid BLOB + NOT NULL + UNIQUE, + name VARCHAR(128) +); + +CREATE TABLE macro_actions +( + macro_id INTEGER + NOT NULL + REFERENCES macros (id) + ON DELETE CASCADE, + relay_id INTEGER + REFERENCES relays (id) + ON DELETE CASCADE, + schedule_id INTEGER + REFERENCES schedules (id) + ON DELETE CASCADE, + weekday SMALLINT + NOT NULL +); diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..c3b4c2c --- /dev/null +++ b/src/db.rs @@ -0,0 +1,62 @@ +pub mod errors; +pub mod models; +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"); + +fn get_connection() -> SqliteConnection { + dotenv().ok(); + + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + SqliteConnection::establish(&database_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) +} + +pub fn run_migrations() { + let connection = get_connection(); + embedded_migrations::run(&connection).expect("Failed to run migrations."); +} + +pub fn get_schedules() -> Vec { + let connection = get_connection(); + schedules + .limit(5) + .load::(&connection) + .expect("Error loading schedules") +} + +pub fn create_schedule(new_name: &str) -> Result { + let connection = get_connection(); + + let new_schedule = NewSchedule { + uid: &EmgauwaUid::default(), + name: new_name, + periods: "", + }; + + diesel::insert_into(schedules) + .values(&new_schedule) + .execute(&connection) + .or(Err(DatabaseError::InsertError))?; + + let result = schedules + .find(sql("last_insert_rowid()")) + .get_result::(&connection) + .or(Err(DatabaseError::InsertGetError))?; + + Ok(result) +} diff --git a/src/db/errors.rs b/src/db/errors.rs new file mode 100644 index 0000000..edf7414 --- /dev/null +++ b/src/db/errors.rs @@ -0,0 +1,30 @@ +use serde::ser::SerializeStruct; +use serde::{Serialize, Serializer}; + +pub enum DatabaseError { + InsertError, + InsertGetError, +} + +impl Serialize for DatabaseError { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut s = serializer.serialize_struct("error", 2)?; + s.serialize_field("code", &500)?; + s.serialize_field("description", &String::from(self))?; + s.end() + } +} + +impl From<&DatabaseError> for String { + fn from(err: &DatabaseError) -> Self { + match err { + DatabaseError::InsertError => String::from("error inserting into database"), + DatabaseError::InsertGetError => { + String::from("error retrieving new entry from database (your entry was saved)") + } + } + } +} diff --git a/src/db/models.rs b/src/db/models.rs new file mode 100644 index 0000000..4f61870 --- /dev/null +++ b/src/db/models.rs @@ -0,0 +1,20 @@ +use super::types::EmgauwaUid; +use serde::Serialize; + +use super::schema::schedules; + +#[derive(Serialize, Queryable)] +pub struct Schedule { + pub id: i32, + pub uid: EmgauwaUid, + pub name: String, + pub periods: String, +} + +#[derive(Insertable)] +#[table_name = "schedules"] +pub struct NewSchedule<'a> { + pub uid: &'a EmgauwaUid, + pub name: &'a str, + pub periods: &'a str, +} diff --git a/src/db/schema.rs b/src/db/schema.rs new file mode 100644 index 0000000..50e04a1 --- /dev/null +++ b/src/db/schema.rs @@ -0,0 +1,93 @@ +table! { + controllers (id) { + id -> Integer, + uid -> Text, + name -> Nullable, + ip -> Nullable, + port -> Nullable, + relay_count -> Nullable, + active -> Bool, + } +} + +table! { + junction_relay_schedule (id) { + id -> Integer, + weekday -> SmallInt, + relay_id -> Nullable, + schedule_id -> Nullable, + } +} + +table! { + junction_tag (id) { + id -> Integer, + tag_id -> Integer, + relay_id -> Nullable, + schedule_id -> Nullable, + } +} + +table! { + macro_actions (id) { + id -> Integer, + macro_id -> Integer, + relay_id -> Nullable, + schedule_id -> Nullable, + weekday -> SmallInt, + } +} + +table! { + macros (id) { + id -> Integer, + uid -> Text, + name -> Nullable, + } +} + +table! { + relays (id) { + id -> Integer, + name -> Nullable, + number -> Integer, + controller_id -> Integer, + } +} + +table! { + schedules (id) { + id -> Integer, + uid -> Binary, + name -> Text, + periods -> Text, + } +} + +table! { + tags (id) { + id -> Integer, + tag -> Text, + } +} + +joinable!(junction_relay_schedule -> relays (relay_id)); +joinable!(junction_relay_schedule -> schedules (schedule_id)); +joinable!(junction_tag -> relays (relay_id)); +joinable!(junction_tag -> schedules (schedule_id)); +joinable!(junction_tag -> tags (tag_id)); +joinable!(macro_actions -> macros (macro_id)); +joinable!(macro_actions -> relays (relay_id)); +joinable!(macro_actions -> schedules (schedule_id)); +joinable!(relays -> controllers (controller_id)); + +allow_tables_to_appear_in_same_query!( + controllers, + junction_relay_schedule, + junction_tag, + macro_actions, + macros, + relays, + schedules, + tags, +); diff --git a/src/db/types.rs b/src/db/types.rs new file mode 100644 index 0000000..8e43cbc --- /dev/null +++ b/src/db/types.rs @@ -0,0 +1,97 @@ +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::{Serialize, Serializer}; +use std::fmt::{Debug, Formatter}; +use std::io::Write; +use uuid::Uuid; + +#[derive(AsExpression, FromSqlRow, PartialEq, Clone)] +#[sql_type = "Binary"] +pub enum EmgauwaUid { + On, + Off, + Any(Uuid), +} + +impl Default for EmgauwaUid { + fn default() -> Self { + EmgauwaUid::Any(Uuid::new_v4()) + } +} +impl Debug for EmgauwaUid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EmgauwaUid::On => "on".fmt(f), + EmgauwaUid::Off => "off".fmt(f), + EmgauwaUid::Any(value) => value.fmt(f), + } + } +} +impl ToSql for EmgauwaUid { + fn to_sql(&self, out: &mut Output) -> serialize::Result { + match self { + EmgauwaUid::On => out.write_all(&[1])?, + EmgauwaUid::Off => out.write_all(&[0])?, + EmgauwaUid::Any(_) => out.write_all(Uuid::from(self).as_bytes())?, + } + Ok(IsNull::No) + } +} +impl FromSql for EmgauwaUid { + fn from_sql(bytes: Option<&::RawValue>) -> deserialize::Result { + match bytes { + None => Ok(EmgauwaUid::default()), + Some(value) => match value.read_blob() { + [0] => Ok(EmgauwaUid::Off), + [1] => Ok(EmgauwaUid::On), + value_bytes => Ok(EmgauwaUid::Any(Uuid::from_slice(value_bytes).unwrap())), + }, + } + } +} + +impl Serialize for EmgauwaUid { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + EmgauwaUid::On => "off".serialize(serializer), + EmgauwaUid::Off => "on".serialize(serializer), + EmgauwaUid::Any(value) => value.serialize(serializer), + } + } +} +impl From for EmgauwaUid { + fn from(uid: Uuid) -> EmgauwaUid { + match uid.as_u128() { + 0 => EmgauwaUid::Off, + 1 => EmgauwaUid::On, + _ => EmgauwaUid::Any(uid), + } + } +} + +impl From<&EmgauwaUid> for Uuid { + fn from(emgauwa_uid: &EmgauwaUid) -> Uuid { + match emgauwa_uid { + EmgauwaUid::On => uuid::Uuid::from_u128(1), + EmgauwaUid::Off => uuid::Uuid::from_u128(0), + EmgauwaUid::Any(value) => *value, + } + } +} + +impl From<&EmgauwaUid> for String { + fn from(emgauwa_uid: &EmgauwaUid) -> String { + match emgauwa_uid { + EmgauwaUid::On => String::from("off"), + EmgauwaUid::Off => String::from("on"), + EmgauwaUid::Any(value) => value.to_hyphenated().to_string(), + } + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..a3a6d96 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/src/handlers/v1/mod.rs b/src/handlers/v1/mod.rs new file mode 100644 index 0000000..68f52e6 --- /dev/null +++ b/src/handlers/v1/mod.rs @@ -0,0 +1 @@ +pub mod schedules; diff --git a/src/handlers/v1/schedules.rs b/src/handlers/v1/schedules.rs new file mode 100644 index 0000000..ee3042c --- /dev/null +++ b/src/handlers/v1/schedules.rs @@ -0,0 +1,24 @@ +use crate::db; +use actix_web::{HttpResponse, Responder}; + +pub async fn index() -> impl Responder { + let schedules = db::get_schedules(); + HttpResponse::Ok().json(schedules) +} + +pub async fn get() -> impl Responder { + "hello from get schedules by id" +} + +pub async fn add() -> impl Responder { + let new_schedule = db::create_schedule("TEST"); + + match new_schedule { + Ok(ok) => HttpResponse::Ok().json(ok), + Err(err) => HttpResponse::InternalServerError().json(err), + } +} + +pub async fn delete() -> impl Responder { + "hello from delete schedule" +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cdd5ca1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,38 @@ +mod db; +mod handlers; + +#[macro_use] +extern crate diesel; +#[macro_use] +extern crate diesel_migrations; +extern crate dotenv; + +use actix_web::{web, App, HttpServer}; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + db::run_migrations(); + + HttpServer::new(|| { + App::new() + .route( + "/api/v1/schedules", + web::get().to(handlers::v1::schedules::index), + ) + .route( + "/api/v1/schedules", + web::post().to(handlers::v1::schedules::add), + ) + .route( + "/api/v1/schedules/{id}", + web::get().to(handlers::v1::schedules::get), + ) + .route( + "/api/v1/schedules/{id}", + web::delete().to(handlers::v1::schedules::delete), + ) + }) + .bind("127.0.0.1:5000")? + .run() + .await +}