diff --git a/yopa-web/resources/templates/index.html.tera b/yopa-web/resources/templates/index.html.tera index 5205794..5fb9989 100644 --- a/yopa-web/resources/templates/index.html.tera +++ b/yopa-web/resources/templates/index.html.tera @@ -24,6 +24,7 @@ {{model.name}}
Delete model · + Edit model · New relation · New property
diff --git a/yopa-web/resources/templates/model_create.html.tera b/yopa-web/resources/templates/model_create.html.tera index 4b737f3..afa1571 100644 --- a/yopa-web/resources/templates/model_create.html.tera +++ b/yopa-web/resources/templates/model_create.html.tera @@ -16,7 +16,7 @@ Define object
-
+
diff --git a/yopa-web/resources/templates/model_update.html.tera b/yopa-web/resources/templates/model_update.html.tera new file mode 100644 index 0000000..e902e32 --- /dev/null +++ b/yopa-web/resources/templates/model_update.html.tera @@ -0,0 +1,22 @@ +{% extends "_layout" %} + +{% block title -%} +Edit object model +{%- endblock %} + +{% block nav -%} +Home +{%- endblock %} + +{% block content -%} + +

Edit object model {{ model.name }}

+ +
+ +
+ + +
+ +{%- endblock %} diff --git a/yopa-web/resources/templates/property_create.html.tera b/yopa-web/resources/templates/property_create.html.tera index 1af482e..d441145 100644 --- a/yopa-web/resources/templates/property_create.html.tera +++ b/yopa-web/resources/templates/property_create.html.tera @@ -19,17 +19,17 @@ Define property -
+
- +
-
+
- @@ -37,7 +37,7 @@ Define property
-
+
diff --git a/yopa-web/resources/templates/relation_create.html.tera b/yopa-web/resources/templates/relation_create.html.tera index fd4aa5d..e5fabee 100644 --- a/yopa-web/resources/templates/relation_create.html.tera +++ b/yopa-web/resources/templates/relation_create.html.tera @@ -18,20 +18,20 @@ Define relation -
+
-
+
- +
-
+
- {% for m in models %} {% endfor %} diff --git a/yopa-web/src/main.rs b/yopa-web/src/main.rs index fad43a2..c5311ad 100644 --- a/yopa-web/src/main.rs +++ b/yopa-web/src/main.rs @@ -1,27 +1,33 @@ -#[macro_use] extern crate log; -#[macro_use] extern crate actix_web; +#[macro_use] +extern crate actix_web; +#[macro_use] +extern crate log; -use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, HttpRequest}; -use parking_lot::Mutex; -use actix_web::web::{service, scope}; -use actix_web::http::StatusCode; +use std::borrow::Borrow; +use std::collections::HashMap; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::path::{PathBuf, Path}; + +use actix_session::CookieSession; +use actix_web::{App, get, HttpRequest, HttpResponse, HttpServer, post, Responder, web}; +use actix_web::http::StatusCode; +use actix_web::web::{scope, service}; use actix_web_static_files; -use tera::Tera; +use actix_web_static_files::ResourceFiles as StaticFiles; use include_dir::Dir; -use std::sync::Arc; use log::LevelFilter; -use crate::tera_ext::TeraExt; use once_cell::sync::Lazy; -use std::borrow::Borrow; -use std::ops::Deref; -use yopa::{Storage, TypedValue}; -use std::collections::HashMap; -use actix_session::CookieSession; +use parking_lot::Mutex; use rand::Rng; -use actix_web_static_files::ResourceFiles as StaticFiles; +use tera::Tera; + +use yopa::{Storage, TypedValue}; + +use crate::tera_ext::TeraExt; +mod utils; mod tera_ext; mod routes; mod session_ext; @@ -32,13 +38,13 @@ include!(concat!(env!("OUT_DIR"), "/static_files.rs")); // Embed templates static TEMPLATES: include_dir::Dir = include_dir::include_dir!("./resources/templates"); -pub(crate) static TERA : Lazy = Lazy::new(|| { +pub(crate) static TERA: Lazy = Lazy::new(|| { let mut tera = Tera::default(); tera.add_include_dir_templates(&TEMPLATES).unwrap(); // Special filter for the TypedValue map use serde_json::Value; - tera.register_filter("print_typed_value", |v : &Value, _ : &HashMap| -> tera::Result { + tera.register_filter("print_typed_value", |v: &Value, _: &HashMap| -> tera::Result { if v.is_null() { return Ok(v.clone()); } @@ -95,15 +101,20 @@ async fn main() -> std::io::Result<()> { /* Routes */ .service(routes::index) - .service(routes::object_model_create_form) - .service(routes::object_model_create) - .service(routes::relation_model_create_form) - .service(routes::relation_model_create) - .service(routes::property_model_create_form) - .service(routes::property_model_create) - .service(routes::object_model_delete) - .service(routes::relation_model_delete) - .service(routes::property_model_delete) + // + .service(routes::object_model::create_form) + .service(routes::object_model::create) + .service(routes::object_model::update_form) + .service(routes::object_model::update) + .service(routes::object_model::delete) + // + .service(routes::relation_model::create_form) + .service(routes::relation_model::create) + .service(routes::relation_model::delete) + // + .service(routes::property_model::create_form) + .service(routes::property_model::create) + .service(routes::property_model::delete) .service(static_files) .default_service(web::to(|| HttpResponse::NotFound().body("File or endpoint not found"))) }) @@ -141,7 +152,7 @@ fn init_yopa() -> YopaStoreWrapper { optional: false, multiple: true, data_type: DataType::String, - default: None + default: None, }).unwrap(); store.define_property(model::PropertyModel { @@ -151,7 +162,7 @@ fn init_yopa() -> YopaStoreWrapper { optional: false, multiple: false, data_type: DataType::String, - default: None + default: None, }).unwrap(); store.define_property(model::PropertyModel { @@ -161,7 +172,7 @@ fn init_yopa() -> YopaStoreWrapper { optional: true, multiple: true, data_type: DataType::String, - default: Some(TypedValue::String("Pepa Novák".into())) + default: Some(TypedValue::String("Pepa Novák".into())), }).unwrap(); let rel_book_id = store.define_relation(model::RelationModel { @@ -171,7 +182,7 @@ fn init_yopa() -> YopaStoreWrapper { reciprocal_name: "recipes".to_string(), optional: true, multiple: true, - related: id_book + related: id_book, }).unwrap(); store.define_property(model::PropertyModel { @@ -181,7 +192,7 @@ fn init_yopa() -> YopaStoreWrapper { optional: true, multiple: false, data_type: DataType::Integer, - default: None + default: None, }).unwrap(); store.define_relation(model::RelationModel { @@ -191,7 +202,7 @@ fn init_yopa() -> YopaStoreWrapper { reciprocal_name: "related recipe".to_string(), optional: true, multiple: true, - related: id_recipe + related: id_recipe, }).unwrap(); web::Data::new(tokio::sync::RwLock::new(store)) diff --git a/yopa-web/src/routes.rs b/yopa-web/src/routes.rs index a527a83..812dab8 100644 --- a/yopa-web/src/routes.rs +++ b/yopa-web/src/routes.rs @@ -1,71 +1,27 @@ -use actix_web::{web, HttpRequest, Responder, HttpResponse}; -use crate::TERA; -use crate::tera_ext::TeraExt; -use yopa::{Storage, StorageError, ID, DataType, TypedValue}; -use serde::{Deserialize, Serialize}; -use yopa::model::{PropertyModel, RelationModel, ObjectModel}; +use std::fmt::{Debug, Display}; use std::ops::DerefMut; -use actix_session::Session; -use crate::session_ext::SessionExt; use std::str::FromStr; -use std::fmt::{Debug, Display}; -use actix_web::http::header::IntoHeaderValue; - -#[derive(Serialize, Debug)] -struct ObjectModelDisplay<'a> { - id : yopa::ID, - name : &'a str, - properties: Vec<&'a PropertyModel>, - relations: Vec>, - reciprocal_relations: Vec>, -} -#[derive(Serialize, Debug)] -struct RelationModelDisplay<'a> { - model : &'a RelationModel, - related_name : &'a str, - properties: Vec<&'a PropertyModel>, -} - -fn redirect(path : impl IntoHeaderValue) -> actix_web::Result { - Ok(HttpResponse::SeeOther() - .header("location", path) // back - to where? - .finish()) -} - -trait ParseOrBadReq { - fn parse_or_bad_request(&self) -> actix_web::Result - where T: FromStr, - E: Display + Debug + 'static; -} +use actix_session::Session; +use actix_web::{HttpRequest, HttpResponse, Responder, web}; +use actix_web::http::header::IntoHeaderValue; +use serde::{Deserialize, Serialize}; -impl ParseOrBadReq for &str { - fn parse_or_bad_request(&self) -> actix_web::Result - where T: FromStr, - E: Display + Debug + 'static - { - self.parse::() - .map_err(|e| { - error!("Parse error for \"{}\"", self); - actix_web::error::ErrorBadRequest(e) - }) - } -} +use yopa::{DataType, ID, Storage, StorageError, TypedValue}; +use yopa::model::{ObjectModel, PropertyModel, RelationModel}; -impl ParseOrBadReq for String { - fn parse_or_bad_request(&self) -> actix_web::Result - where T: FromStr, - E: Display + Debug + 'static - { - self.as_str() - .parse_or_bad_request() - } -} +use crate::session_ext::SessionExt; +use crate::TERA; +use crate::tera_ext::TeraExt; +use crate::routes::relation_model::RelationModelDisplay; +use crate::routes::object_model::ObjectModelDisplay; +pub(crate) mod object_model; +pub(crate) mod relation_model; +pub(crate) mod property_model; #[get("/")] -pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) -> actix_web::Result { - +pub(crate) async fn index(session: Session, store: crate::YopaStoreWrapper) -> actix_web::Result { let rg = store.read().await; let models_iter = rg.get_object_models(); @@ -86,7 +42,7 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) -> RelationModelDisplay { model: rm, related_name: rg.get_model_name(rm.related), - properties: rprops + properties: rprops, } }).collect::>(); relations.sort_by_key(|d| &d.model.name); @@ -100,7 +56,7 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) -> RelationModelDisplay { model: rm, related_name: rg.get_model_name(rm.object), - properties: rprops + properties: rprops, } }).collect::>(); reciprocal_relations.sort_by_key(|d| &d.model.reciprocal_name); @@ -113,7 +69,7 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) -> name: &om.name, properties, relations, - reciprocal_relations + reciprocal_relations, }) } @@ -125,311 +81,3 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) -> TERA.build_response("index", &ctx) } - -#[get("/model/object/create")] -pub(crate) async fn object_model_create_form(session : Session) -> actix_web::Result { - let mut context = tera::Context::new(); - session.render_flash(&mut context); - - TERA.build_response("model_create", &context) -} - -#[derive(Deserialize)] -pub(crate) struct ObjectModelCreate { - pub name : String, -} - -#[post("/model/object/create")] -pub(crate) async fn object_model_create( - form : web::Form, - store : crate::YopaStoreWrapper, - session : Session -) -> actix_web::Result { - let mut wg = store.write().await; - let form = form.into_inner(); - match wg.define_object(ObjectModel { - id: Default::default(), - name: form.name.clone() - }) { - Ok(_id) => { - debug!("Object created, redirecting to root"); - session.flash_success(format!("Object model \"{}\" created.", form.name)); - redirect("/") - } - Err(e) => { - warn!("Error creating model: {:?}", e); - session.flash_error(e.to_string()); - redirect("/model/object/create") - } - } -} - -#[get("/model/relation/create/{object_id}")] -pub(crate) async fn relation_model_create_form( - object_id : web::Path, - store : crate::YopaStoreWrapper, - session : Session -) -> actix_web::Result { - let mut context = tera::Context::new(); - session.render_flash(&mut context); - - let rg = store.read().await; - - debug!("ID = {}", object_id); - - let object = rg.get_object_model(object_id.parse_or_bad_request()?) - .ok_or_else(|| actix_web::error::ErrorNotFound("No such source object"))?; - - let mut models: Vec<_> = rg.get_object_models().collect(); - - models.sort_by_key(|m| &m.name); - - context.insert("models", &models); - context.insert("object", &object); - - TERA.build_response("relation_create", &context) -} - -#[derive(Deserialize)] -pub(crate) struct RelationModelCreate { - pub object : ID, - pub name : String, - pub reciprocal_name : String, - pub optional : Option, - pub multiple : Option, - pub related : ID, -} - -#[post("/model/relation/create")] -pub(crate) async fn relation_model_create( - form : web::Form, - store : crate::YopaStoreWrapper, - session : Session -) -> actix_web::Result { - let mut wg = store.write().await; - let form = form.into_inner(); - match wg.define_relation(RelationModel { - id: Default::default(), - object: form.object, - name: form.name.clone(), - reciprocal_name: form.reciprocal_name.clone(), - optional: form.optional.unwrap_or_default() != 0, - multiple: form.multiple.unwrap_or_default() != 0, - related: form.related - }) { - Ok(_id) => { - debug!("Relation created, redirecting to root"); - session.flash_success(format!("Relation model \"{}\" created.", form.name)); - redirect("/") - } - Err(e) => { - warn!("Error creating relation model: {:?}", e); - session.flash_error(e.to_string()); - redirect(format!("/model/relation/create/{}", form.object)) - } - } -} - -#[derive(Serialize, Debug)] -struct ObjectOrRelationModelDisplay { - id : ID, - describe : String, -} - -#[get("/model/property/create/{object_id}")] -pub(crate) async fn property_model_create_form( - object_id : web::Path, - store : crate::YopaStoreWrapper, - session : Session -) -> actix_web::Result { - let mut context = tera::Context::new(); - session.render_flash(&mut context); - - let rg = store.read().await; - - debug!("ID = {}", object_id); - - let object = { - let id = object_id.parse_or_bad_request()?; - debug!("Create property for ID={}", id); - if let Some(om) = rg.get_object_model(id) { - ObjectOrRelationModelDisplay { - id: om.id, - describe: format!("object model \"{}\"", om.name), - } - } else if let Some(rm) = rg.get_relation_model(id) { - ObjectOrRelationModelDisplay { - id: rm.id, - describe: format!("relation model \"{}\"", rm.name), - } - } else { - return Err(actix_web::error::ErrorNotFound("No such source object")); - } - }; - - context.insert("object", &object); - TERA.build_response("property_create", &context) -} - -#[derive(Deserialize)] -pub(crate) struct PropertyModelCreate { - pub object : ID, - pub name : String, - pub optional : Option, - pub multiple : Option, - pub data_type : DataType, - /// Default value to be parsed to the data type - /// May be unused if empty and optional - pub default : String, -} - -#[post("/model/property/create")] -pub(crate) async fn property_model_create( - form : web::Form, - store : crate::YopaStoreWrapper, - session : Session -) -> actix_web::Result { - let mut wg = store.write().await; - let form = form.into_inner(); - - let optional = form.optional.unwrap_or_default() != 0; - let multiple = form.multiple.unwrap_or_default() != 0; - - match wg.define_property(PropertyModel { - id: Default::default(), - object: form.object, - name: form.name.clone(), - optional, - multiple, - data_type: form.data_type, - default: { - match form.data_type { - DataType::String => { - if form.default.is_empty() && optional { - None - } else { - Some(TypedValue::String(form.default.into())) - } - } - DataType::Integer => { - if form.default.is_empty() { - if optional { - None - } else { - Some(TypedValue::Integer(0)) - } - } else { - // TODO better error reporting - Some(TypedValue::Integer(form.default.parse() - .map_err(|_| { - actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as integer", form.default)) - })?)) - } - } - DataType::Decimal => { - if form.default.is_empty() { - if optional { - None - } else { - Some(TypedValue::Decimal(0.0)) - } - } else { - // TODO better error reporting - Some(TypedValue::Decimal(form.default.parse() - .map_err(|_| { - actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as decimal", form.default)) - })?)) - } - } - DataType::Boolean => { - if form.default.is_empty() { - if optional { - None - } else { - Some(TypedValue::Boolean(false)) - } - } else { - Some(TypedValue::String(form.default.clone().into()) - .cast_to(DataType::Boolean).map_err(|_| { - actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as boolean", form.default)) - })?) - } - } - } - } - }) { - Ok(_id) => { - debug!("Property created, redirecting to root"); - session.flash_success(format!("Property model \"{}\" created.", form.name)); - redirect("/") - } - Err(e) => { - warn!("Error creating property model: {:?}", e); - session.flash_error(e.to_string()); - redirect(format!("/model/property/create/{}", form.object)) - } - } -} - -#[get("/model/object/delete/{id}")] -pub(crate) async fn object_model_delete( - id : web::Path, - store : crate::YopaStoreWrapper, - session : Session -) -> actix_web::Result { - let mut wg = store.write().await; - match wg.undefine_object(id.parse().map_err(|e| actix_web::error::ErrorBadRequest(e))?) { - Ok(om) => { - debug!("Object model deleted, redirecting to root"); - session.flash_success(format!("Object model \"{}\" deleted.", om.name)); - redirect("/") - } - Err(e) => { - warn!("Error deleting object model: {:?}", e); - session.flash_error(e.to_string()); - redirect("/") // back? - } - } -} - -#[get("/model/relation/delete/{id}")] -pub(crate) async fn relation_model_delete( - id : web::Path, - store : crate::YopaStoreWrapper, - session : Session -) -> actix_web::Result { - let mut wg = store.write().await; - match wg.undefine_relation(id.parse_or_bad_request()?) { - Ok(rm) => { - debug!("Relation deleted, redirecting to root"); - session.flash_success(format!("Relation model \"{}\" deleted.", rm.name)); - redirect("/") - } - Err(e) => { - warn!("Error deleting relation model: {:?}", e); - session.flash_error(e.to_string()); - redirect("/") // back? - } - } -} - -#[get("/model/property/delete/{id}")] -pub(crate) async fn property_model_delete( - id : web::Path, - store : crate::YopaStoreWrapper, - session : Session -) -> actix_web::Result { - let mut wg = store.write().await; - match wg.undefine_property(id.parse_or_bad_request()?) { - Ok(rm) => { - debug!("Property deleted, redirecting to root"); - session.flash_success(format!("Property \"{}\" deleted.", rm.name)); - redirect("/") - } - Err(e) => { - warn!("Error deleting property: {:?}", e); - session.flash_error(e.to_string()); - redirect("/") // back? - } - } -} diff --git a/yopa-web/src/routes/object_model.rs b/yopa-web/src/routes/object_model.rs new file mode 100644 index 0000000..44f3ea0 --- /dev/null +++ b/yopa-web/src/routes/object_model.rs @@ -0,0 +1,135 @@ +use actix_session::Session; +use actix_web::{Responder, web}; +use serde::{Deserialize, Serialize}; + +use yopa::ID; +use yopa::model::{ObjectModel, PropertyModel}; + +use crate::routes::relation_model::RelationModelDisplay; +use crate::session_ext::SessionExt; +use crate::TERA; +use crate::tera_ext::TeraExt; +use crate::utils::redirect; + +#[derive(Serialize, Debug)] +pub(crate) struct ObjectModelDisplay<'a> { + pub(crate) id: yopa::ID, + pub(crate) name: &'a str, + pub(crate) properties: Vec<&'a PropertyModel>, + pub(crate) relations: Vec>, + pub(crate) reciprocal_relations: Vec>, +} + +#[get("/model/object/create")] +pub(crate) async fn create_form(session: Session) -> actix_web::Result { + let mut context = tera::Context::new(); + session.render_flash(&mut context); + + TERA.build_response("model_create", &context) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct ObjectModelForm { + pub name: String, +} + +#[post("/model/object/create")] +pub(crate) async fn create( + form: web::Form, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut wg = store.write().await; + let form = form.into_inner(); + match wg.define_object(ObjectModel { + id: Default::default(), + name: form.name.clone(), + }) { + Ok(_id) => { + debug!("Object created, redirecting to root"); + session.flash_success(format!("Object model \"{}\" created.", form.name)); + redirect("/") + } + Err(e) => { + warn!("Error creating model: {:?}", e); + session.flash_error(e.to_string()); + redirect("/model/object/create") + } + } +} + +#[get("/model/object/update/{model_id}")] +pub(crate) async fn update_form( + model_id: web::Path, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut context = tera::Context::new(); + session.render_flash(&mut context); + let rg = store.read().await; + + let model = rg.get_object_model(*model_id) + .ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?; + + // Re-fill old values + if let Ok(Some(form)) = session.take::("old") { + let mut model = model.clone(); + model.name = form.name; + context.insert("model", &model); + } else { + context.insert("model", model); + } + + TERA.build_response("model_update", &context) +} + +#[post("/model/object/update/{model_id}")] +pub(crate) async fn update( + model_id: web::Path, + form: web::Form, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut wg = store.write().await; + let form = form.into_inner(); + + let id = model_id.into_inner(); + match wg.update_object(ObjectModel { + id, + name: form.name.clone(), + }) { + Ok(_id) => { + debug!("Object updated, redirecting to root"); + session.flash_success(format!("Object model \"{}\" updated.", form.name)); + redirect("/") + } + Err(e) => { + warn!("Error updating model: {:?}", e); + session.flash_error(e.to_string()); + session.set("old", form); + redirect(format!("/model/object/update/{}", id)) + } + } +} + + +#[get("/model/object/delete/{id}")] +pub(crate) async fn delete( + id: web::Path, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut wg = store.write().await; + match wg.undefine_object(id.parse().map_err(|e| actix_web::error::ErrorBadRequest(e))?) { + Ok(om) => { + debug!("Object model deleted, redirecting to root"); + session.flash_success(format!("Object model \"{}\" deleted.", om.name)); + redirect("/") + } + Err(e) => { + warn!("Error deleting object model: {:?}", e); + session.flash_error(e.to_string()); + redirect("/") // back? + } + } +} diff --git a/yopa-web/src/routes/property_model.rs b/yopa-web/src/routes/property_model.rs new file mode 100644 index 0000000..cad601d --- /dev/null +++ b/yopa-web/src/routes/property_model.rs @@ -0,0 +1,168 @@ +use actix_session::Session; +use actix_web::{Responder, web}; +use serde::{Serialize, Deserialize}; + +use yopa::{DataType, ID, TypedValue}; +use yopa::model::{PropertyModel, RelationModel}; + +use crate::session_ext::SessionExt; +use crate::TERA; +use crate::tera_ext::TeraExt; +use crate::utils::{ParseOrBadReq, redirect}; +use crate::routes::relation_model::ObjectOrRelationModelDisplay; + +#[get("/model/property/create/{object_id}")] +pub(crate) async fn create_form( + object_id: web::Path, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut context = tera::Context::new(); + session.render_flash(&mut context); + + let rg = store.read().await; + + debug!("ID = {}", object_id); + + let object = { + let id = object_id.parse_or_bad_request()?; + debug!("Create property for ID={}", id); + if let Some(om) = rg.get_object_model(id) { + ObjectOrRelationModelDisplay { + id: om.id, + describe: format!("object model \"{}\"", om.name), + } + } else if let Some(rm) = rg.get_relation_model(id) { + ObjectOrRelationModelDisplay { + id: rm.id, + describe: format!("relation model \"{}\"", rm.name), + } + } else { + return Err(actix_web::error::ErrorNotFound("No such source object")); + } + }; + + context.insert("object", &object); + TERA.build_response("property_create", &context) +} + +#[derive(Deserialize)] +pub(crate) struct PropertyModelCreate { + pub object: ID, + pub name: String, + pub optional: Option, + pub multiple: Option, + pub data_type: DataType, + /// Default value to be parsed to the data type + /// May be unused if empty and optional + pub default: String, +} + +#[post("/model/property/create")] +pub(crate) async fn create( + form: web::Form, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut wg = store.write().await; + let form = form.into_inner(); + + let optional = form.optional.unwrap_or_default() != 0; + let multiple = form.multiple.unwrap_or_default() != 0; + + match wg.define_property(PropertyModel { + id: Default::default(), + object: form.object, + name: form.name.clone(), + optional, + multiple, + data_type: form.data_type, + default: { + match form.data_type { + DataType::String => { + if form.default.is_empty() && optional { + None + } else { + Some(TypedValue::String(form.default.into())) + } + } + DataType::Integer => { + if form.default.is_empty() { + if optional { + None + } else { + Some(TypedValue::Integer(0)) + } + } else { + // TODO better error reporting + Some(TypedValue::Integer(form.default.parse() + .map_err(|_| { + actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as integer", form.default)) + })?)) + } + } + DataType::Decimal => { + if form.default.is_empty() { + if optional { + None + } else { + Some(TypedValue::Decimal(0.0)) + } + } else { + // TODO better error reporting + Some(TypedValue::Decimal(form.default.parse() + .map_err(|_| { + actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as decimal", form.default)) + })?)) + } + } + DataType::Boolean => { + if form.default.is_empty() { + if optional { + None + } else { + Some(TypedValue::Boolean(false)) + } + } else { + Some(TypedValue::String(form.default.clone().into()) + .cast_to(DataType::Boolean).map_err(|_| { + actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as boolean", form.default)) + })?) + } + } + } + }, + }) { + Ok(_id) => { + debug!("Property created, redirecting to root"); + session.flash_success(format!("Property model \"{}\" created.", form.name)); + redirect("/") + } + Err(e) => { + warn!("Error creating property model: {:?}", e); + session.flash_error(e.to_string()); + redirect(format!("/model/property/create/{}", form.object)) + } + } +} + +#[get("/model/property/delete/{id}")] +pub(crate) async fn delete( + id: web::Path, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut wg = store.write().await; + match wg.undefine_property(id.parse_or_bad_request()?) { + Ok(rm) => { + debug!("Property deleted, redirecting to root"); + session.flash_success(format!("Property \"{}\" deleted.", rm.name)); + redirect("/") + } + Err(e) => { + warn!("Error deleting property: {:?}", e); + session.flash_error(e.to_string()); + redirect("/") // back? + } + } +} diff --git a/yopa-web/src/routes/relation_model.rs b/yopa-web/src/routes/relation_model.rs new file mode 100644 index 0000000..c690e45 --- /dev/null +++ b/yopa-web/src/routes/relation_model.rs @@ -0,0 +1,111 @@ +use actix_session::Session; +use actix_web::{Responder, web}; +use serde::{Serialize, Deserialize}; + +use yopa::ID; +use yopa::model::{PropertyModel, RelationModel}; + +use crate::session_ext::SessionExt; +use crate::TERA; +use crate::tera_ext::TeraExt; +use crate::utils::{ParseOrBadReq, redirect}; + +#[derive(Serialize, Debug)] +pub(crate) struct RelationModelDisplay<'a> { + pub(crate) model: &'a RelationModel, + pub(crate) related_name: &'a str, + pub(crate) properties: Vec<&'a PropertyModel>, +} + +#[get("/model/relation/create/{object_id}")] +pub(crate) async fn create_form( + object_id: web::Path, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut context = tera::Context::new(); + session.render_flash(&mut context); + + let rg = store.read().await; + + debug!("ID = {}", object_id); + + let object = rg.get_object_model(object_id.parse_or_bad_request()?) + .ok_or_else(|| actix_web::error::ErrorNotFound("No such source object"))?; + + let mut models: Vec<_> = rg.get_object_models().collect(); + + models.sort_by_key(|m| &m.name); + + context.insert("models", &models); + context.insert("object", &object); + + TERA.build_response("relation_create", &context) +} + +#[derive(Deserialize)] +pub(crate) struct RelationModelCreate { + pub object: ID, + pub name: String, + pub reciprocal_name: String, + pub optional: Option, + pub multiple: Option, + pub related: ID, +} + +#[post("/model/relation/create")] +pub(crate) async fn create( + form: web::Form, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut wg = store.write().await; + let form = form.into_inner(); + match wg.define_relation(RelationModel { + id: Default::default(), + object: form.object, + name: form.name.clone(), + reciprocal_name: form.reciprocal_name.clone(), + optional: form.optional.unwrap_or_default() != 0, + multiple: form.multiple.unwrap_or_default() != 0, + related: form.related, + }) { + Ok(_id) => { + debug!("Relation created, redirecting to root"); + session.flash_success(format!("Relation model \"{}\" created.", form.name)); + redirect("/") + } + Err(e) => { + warn!("Error creating relation model: {:?}", e); + session.flash_error(e.to_string()); + redirect(format!("/model/relation/create/{}", form.object)) + } + } +} + +#[derive(Serialize, Debug)] +pub(crate) struct ObjectOrRelationModelDisplay { + pub(crate) id: ID, + pub(crate) describe: String, +} + +#[get("/model/relation/delete/{id}")] +pub(crate) async fn delete( + id: web::Path, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut wg = store.write().await; + match wg.undefine_relation(id.parse_or_bad_request()?) { + Ok(rm) => { + debug!("Relation deleted, redirecting to root"); + session.flash_success(format!("Relation model \"{}\" deleted.", rm.name)); + redirect("/") + } + Err(e) => { + warn!("Error deleting relation model: {:?}", e); + session.flash_error(e.to_string()); + redirect("/") // back? + } + } +} diff --git a/yopa-web/src/scratch.rs b/yopa-web/src/scratch.rs deleted file mode 100644 index 38ffb4c..0000000 --- a/yopa-web/src/scratch.rs +++ /dev/null @@ -1,25 +0,0 @@ - - -// #[get("/")] -// async fn hello(state : web::Data) -> impl Responder { -// HttpResponse::Ok().body(format!("Hello world! {}", state.count_visit())) -// } -// -// #[post("/echo")] -// async fn echo(req_body: String) -> impl Responder { -// HttpResponse::Ok().body(req_body) -// } -// async fn static_files(req: HttpRequest) -> actix_web::Result { -// let path: PathBuf = req.match_info().query("filename").parse().unwrap(); -// -// -// -// Ok(NamedFile::open(path)?) -// } -// -// #[get("/users/{user_id}/{friend}")] // <- define path parameters -// async fn user_friend(web::Path((user_id, friend)): web::Path<(u32, String)>, state : web::Data) -> impl Responder { -// HttpResponse::Ok() -// .header("Content-Type", "text/html") -// .body(format!("

Welcome {}, user_id {}!

Visitor nr {}\n", friend, user_id, state.count_visit())) -// } diff --git a/yopa-web/src/session_ext.rs b/yopa-web/src/session_ext.rs index 1ec8ae7..7e0e6bc 100644 --- a/yopa-web/src/session_ext.rs +++ b/yopa-web/src/session_ext.rs @@ -1,15 +1,15 @@ -use serde::de::DeserializeOwned; use actix_session::Session; +use serde::de::DeserializeOwned; pub trait SessionExt { /// Get a `value` from the session. fn take(&self, key: &str) -> Result, actix_web::error::Error>; - fn render_flash(&self, context : &mut tera::Context); + fn render_flash(&self, context: &mut tera::Context); - fn flash_error(&self, msg : impl AsRef); + fn flash_error(&self, msg: impl AsRef); - fn flash_success(&self, msg : impl AsRef); + fn flash_success(&self, msg: impl AsRef); } impl SessionExt for Session { @@ -19,7 +19,7 @@ impl SessionExt for Session { Ok(val?) } - fn render_flash(&self, context : &mut tera::Context) { + fn render_flash(&self, context: &mut tera::Context) { if let Ok(Some(msg)) = self.take::("flash_error") { context.insert("flash_error", &msg); } @@ -29,11 +29,11 @@ impl SessionExt for Session { } } - fn flash_error(&self, msg : impl AsRef) { + fn flash_error(&self, msg: impl AsRef) { self.set("flash_error", msg.as_ref()).unwrap(); } - fn flash_success(&self, msg : impl AsRef) { + fn flash_success(&self, msg: impl AsRef) { self.set("flash_success", msg.as_ref()).unwrap(); } } diff --git a/yopa-web/src/tera_ext.rs b/yopa-web/src/tera_ext.rs index 1a6d909..22ee1f2 100644 --- a/yopa-web/src/tera_ext.rs +++ b/yopa-web/src/tera_ext.rs @@ -1,11 +1,11 @@ -use tera::Tera; -use include_dir::Dir; -use actix_web::HttpResponse; use actix_web::http::StatusCode; +use actix_web::HttpResponse; +use include_dir::Dir; +use tera::Tera; -fn tera_includedir_walk_folder_inner(collected : &mut Vec<(String, String)>, tera : &mut Tera, dir: &Dir) { +fn tera_includedir_walk_folder_inner(collected: &mut Vec<(String, String)>, tera: &mut Tera, dir: &Dir) { for f in dir.files() { - let halves : Vec<_> = f.path().file_name().unwrap().to_str().unwrap().split('.').collect(); + let halves: Vec<_> = f.path().file_name().unwrap().to_str().unwrap().split('.').collect(); if halves.last().unwrap() != &"tera" { debug!("Bad file: {:?}", f); @@ -30,11 +30,11 @@ fn tera_includedir_walk_folder_inner(collected : &mut Vec<(String, String)>, ter pub(crate) trait TeraExt { fn add_include_dir_templates(&mut self, dir: &Dir) -> tera::Result<()>; - fn build_response(&self, template: &str, context : &tera::Context) -> actix_web::Result { + fn build_response(&self, template: &str, context: &tera::Context) -> actix_web::Result { self.build_err_response(StatusCode::OK, template, context) } - fn build_err_response(&self, code : StatusCode, template: &str, context : &tera::Context) -> actix_web::Result; + fn build_err_response(&self, code: StatusCode, template: &str, context: &tera::Context) -> actix_web::Result; } impl TeraExt for Tera { @@ -45,7 +45,7 @@ impl TeraExt for Tera { self.add_raw_templates(templates) } - fn build_err_response(&self, code : StatusCode, template: &str, context : &tera::Context) -> actix_web::Result { + fn build_err_response(&self, code: StatusCode, template: &str, context: &tera::Context) -> actix_web::Result { let html = self.render(template, context).map_err(|e| { actix_web::error::ErrorInternalServerError(e) })?; diff --git a/yopa-web/src/utils.rs b/yopa-web/src/utils.rs new file mode 100644 index 0000000..c482837 --- /dev/null +++ b/yopa-web/src/utils.rs @@ -0,0 +1,39 @@ +use actix_web::http::header::IntoHeaderValue; +use actix_web::HttpResponse; +use std::str::FromStr; +use std::fmt::{Display, Debug}; + +pub fn redirect(path: impl IntoHeaderValue) -> actix_web::Result { + Ok(HttpResponse::SeeOther() + .header("location", path) // back - to where? + .finish()) +} + +pub trait ParseOrBadReq { + fn parse_or_bad_request(&self) -> actix_web::Result + where T: FromStr, + E: Display + Debug + 'static; +} + +impl ParseOrBadReq for &str { + fn parse_or_bad_request(&self) -> actix_web::Result + where T: FromStr, + E: Display + Debug + 'static + { + self.parse::() + .map_err(|e| { + error!("Parse error for \"{}\"", self); + actix_web::error::ErrorBadRequest(e) + }) + } +} + +impl ParseOrBadReq for String { + fn parse_or_bad_request(&self) -> actix_web::Result + where T: FromStr, + E: Display + Debug + 'static + { + self.as_str() + .parse_or_bad_request() + } +} diff --git a/yopa/src/lib.rs b/yopa/src/lib.rs index 7523a17..75f2e31 100644 --- a/yopa/src/lib.rs +++ b/yopa/src/lib.rs @@ -375,4 +375,18 @@ impl Storage { self.rel_models.values() .into_group_map_by(|model| model.related) } + + // Updates + pub fn update_object(&mut self, model : ObjectModel) -> Result<(), StorageError> { + if !self.obj_models.contains_key(&model.id) { + return Err(StorageError::NotExist(format!("Object model ID {} does not exist.", model.id).into())); + } + + if let Some(conflict) = self.obj_models.values().find(|m| m.id != model.id && m.name == model.name) { + return Err(StorageError::ConstraintViolation(format!("Object {} already has the name {}", conflict.id, model.name).into())); + } + + self.obj_models.insert(model.id, model); + Ok(()) + } }