From 8b87fd00794a8cfdc10f1d9ba71ebb0ed164ee83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 7 Feb 2021 01:14:14 +0100 Subject: [PATCH] show model overview in index page --- Cargo.lock | 5 +- yopa-web/Cargo.toml | 6 +- yopa-web/resources/index.tmp | 64 +++++++++ yopa-web/resources/static/style.css | 7 + yopa-web/resources/templates/index.html.tera | 65 ++++++++- .../templates/model_create.html.tera | 30 ++++ .../templates/property_create.html.tera | 42 ++++++ yopa-web/src/main.rs | 132 +++++++++++++++--- yopa-web/src/routes.rs | 68 +++++++++ yopa-web/src/tera_ext.rs | 18 ++- yopa/src/data.rs | 13 ++ yopa/src/lib.rs | 45 +++++- 12 files changed, 466 insertions(+), 29 deletions(-) create mode 100644 yopa-web/resources/index.tmp create mode 100644 yopa-web/resources/templates/model_create.html.tera create mode 100644 yopa-web/resources/templates/property_create.html.tera create mode 100644 yopa-web/src/routes.rs diff --git a/Cargo.lock b/Cargo.lock index 076fbfa..81f4c05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2378,10 +2378,13 @@ dependencies = [ "actix-web", "actix-web-static-files", "include_dir", - "lazy_static", "log", + "once_cell", "parking_lot", + "serde", + "serde_json", "simple-logging", "tera", "tokio", + "yopa", ] diff --git a/yopa-web/Cargo.toml b/yopa-web/Cargo.toml index d98a727..1d27787 100644 --- a/yopa-web/Cargo.toml +++ b/yopa-web/Cargo.toml @@ -8,6 +8,10 @@ build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +yopa = { path = "../yopa" } +serde = "1" +serde_json = "1" + log = "0.4.14" simple-logging = "2.0.2" actix-web = "3" @@ -15,7 +19,7 @@ parking_lot = "0.11.1" include_dir = "0.6.0" tera = "1.6.1" actix-web-static-files = "3.0" -lazy_static = "1.4.0" +once_cell = "1.5.2" tokio = { version="0.2.6", features=["full"] } diff --git a/yopa-web/resources/index.tmp b/yopa-web/resources/index.tmp new file mode 100644 index 0000000..ad1aff5 --- /dev/null +++ b/yopa-web/resources/index.tmp @@ -0,0 +1,64 @@ +{% extends "_layout" %} + +{% block title -%} + Index +{%- endblock %} + +{% block nav -%} +Home +{%- endblock %} + +{% block content -%} + +

Welcome to tera on actix

+ +New model + +

Defined models:

+ + + +{%- endblock %} diff --git a/yopa-web/resources/static/style.css b/yopa-web/resources/static/style.css index 310b245..0d3d425 100644 --- a/yopa-web/resources/static/style.css +++ b/yopa-web/resources/static/style.css @@ -119,6 +119,8 @@ textarea:focus, outline: 0 none !important; } +/* + .cards-table { border-collapse: collapse; margin: 0 auto; @@ -198,3 +200,8 @@ textarea:focus, background: transparent; } +*/ + +li { + padding-bottom: .5rem; +} diff --git a/yopa-web/resources/templates/index.html.tera b/yopa-web/resources/templates/index.html.tera index d9114f9..999ffb0 100644 --- a/yopa-web/resources/templates/index.html.tera +++ b/yopa-web/resources/templates/index.html.tera @@ -5,11 +5,72 @@ {%- endblock %} {% block nav -%} - Home +Home {%- endblock %} {% block content -%} -

Welcome to tera on actix

+

Welcome to YOPA

+ +New model + +

Defined models:

+ + {%- endblock %} diff --git a/yopa-web/resources/templates/model_create.html.tera b/yopa-web/resources/templates/model_create.html.tera new file mode 100644 index 0000000..0a314f1 --- /dev/null +++ b/yopa-web/resources/templates/model_create.html.tera @@ -0,0 +1,30 @@ +{% extends "_layout" %} + +{% block title -%} +Define object +{%- endblock %} + +{% block nav -%} +Home +{%- endblock %} + +{% block content -%} + +

Define new object model

+ +
+ +
+ + +
+ + +
+ +{%- endblock %} diff --git a/yopa-web/resources/templates/property_create.html.tera b/yopa-web/resources/templates/property_create.html.tera new file mode 100644 index 0000000..625f5db --- /dev/null +++ b/yopa-web/resources/templates/property_create.html.tera @@ -0,0 +1,42 @@ +{% extends "_layout" %} + +{% block title -%} +Define property +{%- endblock %} + +{% block nav -%} +Home +{%- endblock %} + +{% block content -%} + +

Add new property to model "{{model.name}}"

+ +
+ + + +
+ + + +
+ + +
+ + +
+ + +
+ + +
+ +{%- endblock %} diff --git a/yopa-web/src/main.rs b/yopa-web/src/main.rs index 9663d7e..d00d4f5 100644 --- a/yopa-web/src/main.rs +++ b/yopa-web/src/main.rs @@ -1,5 +1,5 @@ #[macro_use] extern crate log; -#[macro_use] extern crate lazy_static; +#[macro_use] extern crate actix_web; use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, HttpRequest}; use parking_lot::Mutex; @@ -13,8 +13,14 @@ 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; mod tera_ext; +mod routes; // Embed static files include!(concat!(env!("OUT_DIR"), "/static_files.rs")); @@ -22,35 +28,127 @@ include!(concat!(env!("OUT_DIR"), "/static_files.rs")); // Embed templates static TEMPLATES: include_dir::Dir = include_dir::include_dir!("./resources/templates"); -lazy_static! { - static ref TERA : Tera = { - let mut tera = Tera::default(); - tera.add_include_dir_templates(&TEMPLATES); - tera - }; -} +pub(crate) static TERA : Lazy = Lazy::new(|| { + let mut tera = Tera::default(); + tera.add_include_dir_templates(&TEMPLATES); -#[get("/")] -async fn service_index(req: HttpRequest) -> actix_web::Result { - let mut context = tera::Context::new(); + // Special filter for the TypedValue map + use serde_json::Value; + tera.register_filter("print_typed_value", |v : &Value, _ : &HashMap| -> tera::Result { + if v.is_null() { + return Ok(v.clone()); + } + if let Value::Object(map) = v { + if let Some((_, v)) = map.iter().next() { + return Ok(v.clone()); + } + } + Err(tera::Error::msg("Expected nonenmpty object")) + }); - let html = TERA.render("index", &context).map_err(|e| { - actix_web::error::ErrorInternalServerError(e) - })?; + tera +}); - Ok(HttpResponse::Ok().body(html)) -} +type YopaStoreWrapper = web::Data>; #[actix_web::main] async fn main() -> std::io::Result<()> { simple_logging::log_to_stderr(LevelFilter::Debug); + // Ensure the lazy ref is initialized early (to catch template bugs ASAP) + let _ = TERA.deref(); + + let database : YopaStoreWrapper = { + let mut store = Storage::new(); + + // Seed the store with some dummy data for view development + use yopa::model; + use yopa::DataType; + + let id_recipe = store.define_object(model::ObjectModel { + id: Default::default(), + name: "Recipe".to_string(), + parent: None + }).unwrap(); + let id_book = store.define_object(model::ObjectModel { + id: Default::default(), + name: "Book".to_string(), + parent: None + }).unwrap(); + let id_ing = store.define_object(model::ObjectModel { + id: Default::default(), + name: "Ingredient".to_string(), + parent: None + }).unwrap(); + + store.define_property(model::PropertyModel { + id: Default::default(), + object: id_recipe, + name: "name".to_string(), + optional: false, + multiple: true, + data_type: DataType::String, + default: None + }).unwrap(); + + store.define_property(model::PropertyModel { + id: Default::default(), + object: id_book, + name: "title".to_string(), + optional: false, + multiple: false, + data_type: DataType::String, + default: None + }).unwrap(); + + store.define_property(model::PropertyModel { + id: Default::default(), + object: id_book, + name: "author".to_string(), + optional: true, + multiple: true, + data_type: DataType::String, + default: Some(TypedValue::String("Pepa Novák".into())) + }).unwrap(); + + let rel_book_id = store.define_relation(model::RelationModel { + id: Default::default(), + object: id_recipe, + name: "book reference".to_string(), + optional: true, + multiple: true, + related: id_book + }).unwrap(); + + store.define_property(model::PropertyModel { + id: Default::default(), + object: rel_book_id, + name: "page".to_string(), + optional: true, + multiple: false, + data_type: DataType::Integer, + default: None + }).unwrap(); + + store.define_relation(model::RelationModel { + id: Default::default(), + object: id_recipe, + name: "related recipe".to_string(), + optional: true, + multiple: true, + related: id_recipe + }).unwrap(); + + web::Data::new(tokio::sync::RwLock::new(store)) + }; + HttpServer::new(move || { let static_files = actix_web_static_files::ResourceFiles::new("/static", included_static_files()) .do_not_resolve_defaults(); App::new() - .service(service_index) + .app_data(database.clone()) + .service(routes::index) .service(static_files) }) .bind("127.0.0.1:8080")?.run().await diff --git a/yopa-web/src/routes.rs b/yopa-web/src/routes.rs new file mode 100644 index 0000000..4256021 --- /dev/null +++ b/yopa-web/src/routes.rs @@ -0,0 +1,68 @@ +use actix_web::{web, HttpRequest, Responder}; +use crate::TERA; +use crate::tera_ext::TeraExt; +use yopa::Storage; +use serde::Serialize; +use yopa::model::{PropertyModel, RelationModel}; + +#[derive(Serialize, Debug)] +struct ObjectModelDisplay<'a> { + id : yopa::ID, + name : &'a str, + properties: Vec<&'a PropertyModel>, + relations: Vec>, +} + +#[derive(Serialize, Debug)] +struct RelationModelDisplay<'a> { + model : &'a RelationModel, + related_name : &'a str, + properties: Vec<&'a PropertyModel>, +} + +#[get("/")] +pub(crate) async fn index(req: HttpRequest, store : crate::YopaStoreWrapper) -> actix_web::Result { + + let rg = store.read().await; + + let models_iter = rg.get_object_models(); + + // object and relation props + let mut model_props = rg.get_grouped_prop_models(); + + let mut model_relations = rg.get_grouped_relation_models(); + + let mut models = vec![]; + for om in models_iter { + let mut oprops = model_props.remove(&om.id).unwrap_or_default(); + let mut relations = model_relations.remove(&om.id).unwrap_or_default(); + + let rel_displays = relations.into_iter().map(|rm| { + let mut rprops = model_props.remove(&rm.id).unwrap_or_default(); + rprops.sort_by_key(|m| &m.name); + + RelationModelDisplay { + model: rm, + related_name: rg.get_model_name(rm.related), + properties: rprops + } + + }).collect::>(); + + oprops.sort_by_key(|m| &m.name); + + models.push(ObjectModelDisplay { + id: om.id, + name: &om.name, + properties: oprops, + relations: rel_displays, + }) + } + + models.sort_by_key(|m| m.name); + + let mut ctx = tera::Context::new(); + ctx.insert("models", &models); + + TERA.build_response("index", &ctx) +} diff --git a/yopa-web/src/tera_ext.rs b/yopa-web/src/tera_ext.rs index 8cf1495..1a6d909 100644 --- a/yopa-web/src/tera_ext.rs +++ b/yopa-web/src/tera_ext.rs @@ -1,5 +1,7 @@ use tera::Tera; use include_dir::Dir; +use actix_web::HttpResponse; +use actix_web::http::StatusCode; fn tera_includedir_walk_folder_inner(collected : &mut Vec<(String, String)>, tera : &mut Tera, dir: &Dir) { for f in dir.files() { @@ -27,13 +29,27 @@ 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 { + self.build_err_response(StatusCode::OK, template, context) + } + + fn build_err_response(&self, code : StatusCode, template: &str, context : &tera::Context) -> actix_web::Result; } impl TeraExt for Tera { fn add_include_dir_templates(&mut self, dir: &Dir) -> tera::Result<()> { - debug!("Walk dir: {:?}", dir); + debug!("Walk dir: {:?}", dir.path()); let mut templates = vec![]; tera_includedir_walk_folder_inner(&mut templates, self, dir); self.add_raw_templates(templates) } + + 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) + })?; + + Ok(HttpResponse::build(code).body(html)) + } } diff --git a/yopa/src/data.rs b/yopa/src/data.rs index 44155f8..1e59724 100644 --- a/yopa/src/data.rs +++ b/yopa/src/data.rs @@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize}; use crate::ID; use crate::model::DataType; use crate::id::HaveId; +use std::fmt::{Display, Formatter}; +use std::fmt; /// Value of a particular type #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -21,6 +23,17 @@ pub enum TypedValue { Boolean(bool), } +impl Display for TypedValue { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + TypedValue::String(v) => write!(f, "{}", v), + TypedValue::Integer(v) => write!(f, "{}", v), + TypedValue::Decimal(v) => write!(f, "{}", v), + TypedValue::Boolean(v) => write!(f, "{}", if *v { "True" } else { "False" }), + } + } +} + impl TypedValue { /// Try ot cast to another type. On error, the original value is returned as Err. pub fn cast_to(self, ty: DataType) -> Result { diff --git a/yopa/src/lib.rs b/yopa/src/lib.rs index 49456aa..a338e1b 100644 --- a/yopa/src/lib.rs +++ b/yopa/src/lib.rs @@ -13,6 +13,10 @@ use id::next_id; use insert::InsertObj; use insert::InsertValue; use model::ObjectModel; +use crate::model::{PropertyModel, RelationModel}; + +pub use data::{TypedValue}; +pub use model::{DataType}; pub mod model; pub mod data; @@ -26,7 +30,7 @@ mod serde_map_as_list; /// Stupid storage with no persistence #[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct InMemoryStorage { +pub struct Storage { #[serde(with = "serde_map_as_list")] obj_models: HashMap, #[serde(with = "serde_map_as_list")] @@ -51,7 +55,7 @@ pub enum StorageError { ConstraintViolation(Cow<'static, str>), } -impl InMemoryStorage { +impl Storage { /// Create empty store pub fn new() -> Self { Self::default() @@ -181,7 +185,7 @@ impl InMemoryStorage { }; } - pub fn describe_id(&self, id: ID) -> String { + pub fn describe_model(&self, id: ID) -> String { if let Some(x) = self.obj_models.get(&id) { x.to_string() } else if let Some(x) = self.rel_models.get(&id) { @@ -193,6 +197,18 @@ impl InMemoryStorage { } } + pub fn get_model_name(&self, id: ID) -> &str { + if let Some(x) = self.obj_models.get(&id) { + &x.name + } else if let Some(x) = self.rel_models.get(&id) { + &x.name + } else if let Some(x) = self.prop_models.get(&id) { + &x.name + } else { + "???" + } + } + // DATA /// Insert object with relations, validating the data model constraints @@ -217,7 +233,7 @@ impl InMemoryStorage { for (id, prop) in self.prop_models.iter().filter(|(_id, p)| p.object == parent_model_id) { if let Some(values) = values_by_id.remove(id) { if values.len() > 1 && !prop.multiple { - return Err(StorageError::ConstraintViolation(format!("{} of {} cannot have multiple values", prop, self.describe_id(parent_model_id)).into())); + return Err(StorageError::ConstraintViolation(format!("{} of {} cannot have multiple values", prop, self.describe_model(parent_model_id)).into())); } for val_instance in values { values_to_insert.push(data::Value { @@ -238,7 +254,7 @@ impl InMemoryStorage { value: def.clone(), }); } else { - return Err(StorageError::ConstraintViolation(format!("{} is required for {} and no default value is defined", prop, self.describe_id(parent_model_id)).into())); + return Err(StorageError::ConstraintViolation(format!("{} is required for {} and no default value is defined", prop, self.describe_model(parent_model_id)).into())); } } } @@ -265,8 +281,8 @@ impl InMemoryStorage { return Err(StorageError::ConstraintViolation( format!("{} of {} requires object of type {}, got {}", relation_model, obj_model, - self.describe_id(relation_model.related), - self.describe_id(related.model)).into())); + self.describe_model(relation_model.related), + self.describe_model(related.model)).into())); } } @@ -301,4 +317,19 @@ impl InMemoryStorage { Ok(object_id) } + + // Reading + pub fn get_object_models(&self) -> impl Iterator { + self.obj_models.values() + } + + pub fn get_grouped_prop_models(&self) -> HashMap> { + self.prop_models.values() + .into_group_map_by(|model| model.object) + } + + pub fn get_grouped_relation_models(&self) -> HashMap> { + self.rel_models.values() + .into_group_map_by(|model| model.object) + } }