show model overview in index page

master
Ondřej Hruška 4 years ago
parent 4e97f7a19a
commit 8b87fd0079
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 5
      Cargo.lock
  2. 6
      yopa-web/Cargo.toml
  3. 64
      yopa-web/resources/index.tmp
  4. 7
      yopa-web/resources/static/style.css
  5. 65
      yopa-web/resources/templates/index.html.tera
  6. 30
      yopa-web/resources/templates/model_create.html.tera
  7. 42
      yopa-web/resources/templates/property_create.html.tera
  8. 128
      yopa-web/src/main.rs
  9. 68
      yopa-web/src/routes.rs
  10. 18
      yopa-web/src/tera_ext.rs
  11. 13
      yopa/src/data.rs
  12. 45
      yopa/src/lib.rs

5
Cargo.lock generated

@ -2378,10 +2378,13 @@ dependencies = [
"actix-web", "actix-web",
"actix-web-static-files", "actix-web-static-files",
"include_dir", "include_dir",
"lazy_static",
"log", "log",
"once_cell",
"parking_lot", "parking_lot",
"serde",
"serde_json",
"simple-logging", "simple-logging",
"tera", "tera",
"tokio", "tokio",
"yopa",
] ]

@ -8,6 +8,10 @@ build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
yopa = { path = "../yopa" }
serde = "1"
serde_json = "1"
log = "0.4.14" log = "0.4.14"
simple-logging = "2.0.2" simple-logging = "2.0.2"
actix-web = "3" actix-web = "3"
@ -15,7 +19,7 @@ parking_lot = "0.11.1"
include_dir = "0.6.0" include_dir = "0.6.0"
tera = "1.6.1" tera = "1.6.1"
actix-web-static-files = "3.0" actix-web-static-files = "3.0"
lazy_static = "1.4.0" once_cell = "1.5.2"
tokio = { version="0.2.6", features=["full"] } tokio = { version="0.2.6", features=["full"] }

@ -0,0 +1,64 @@
{% extends "_layout" %}
{% block title -%}
Index
{%- endblock %}
{% block nav -%}
<a href="/">Home</a>
{%- endblock %}
{% block content -%}
<h1>Welcome to tera on actix</h1>
<a href="/model/object/create">New model</a>
<h2>Defined models:</h2>
<ul>
{% for model in models %}
<li>
<b>{{model.name}}</b><br>
{#
{% if !model.properties.is_empty() %}
Properties:
<ul>
{% for prop in model.properties %}
<li>{{prop.name}}, {{prop.data_type}}, def: {{prop.default}}, opt: {{prop.optional}}, mult: {{prop.multiple}}</li>
{% endfor %}
</ul>
{% endif %}
<a href="/model/property/create/{{model.id}}">New property</a>
{% if !model.relations.is_empty() %}
Relations:
<ul>
{% for rel in model.relations %}
<li>
{{rel.name}} -> {{rel.related_name}}
{% if !rel.properties.is_empty() %}
Properties:
<ul>
{% for prop in rel.properties %}
<li>{{prop.name}}, {{prop.data_type}}, def: {{prop.default}}, opt: {{prop.optional}}, mult: {{prop.multiple}}</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
<a href="/model/relation/create/{{model.id}}">New relation</a>
#}
</li>
{% endfor %}
</ul>
{%- endblock %}

@ -119,6 +119,8 @@ textarea:focus,
outline: 0 none !important; outline: 0 none !important;
} }
/*
.cards-table { .cards-table {
border-collapse: collapse; border-collapse: collapse;
margin: 0 auto; margin: 0 auto;
@ -198,3 +200,8 @@ textarea:focus,
background: transparent; background: transparent;
} }
*/
li {
padding-bottom: .5rem;
}

@ -5,11 +5,72 @@
{%- endblock %} {%- endblock %}
{% block nav -%} {% block nav -%}
<a href="/">Home</a> <a href="/">Home</a>
{%- endblock %} {%- endblock %}
{% block content -%} {% block content -%}
<h1>Welcome to tera on actix</h1> <h1>Welcome to YOPA</h1>
<a href="/model/object/create">New model</a>
<h2>Defined models:</h2>
<ul>
{% for model in models %}
<li>
<b title="{{model.id}}">{{model.name}}</b><br>
{% if model.properties %}
Properties:
<ul>
{% for prop in model.properties %}
<li>
{{prop.name}}, {{prop.data_type}}
{%- if prop.default -%}
, default: "{{prop.default | print_typed_value}}"
{%- endif -%}
{%- if prop.optional %}, OPTIONAL{% endif %}
{%- if prop.multiple %}, MULTIPLE{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
<a href="/model/property/create/{{model.id}}">Add a property</a><br>
{% if model.relations %}
Relations:
<ul>
{% for rel in model.relations %}
<li>
<span title="{{rel.model.id}}">"{{rel.model.name}}", pointing to: <i>{{rel.related_name}}</i></span><br>
{% if rel.properties %}
Properties:
<ul>
{% for prop in rel.properties %}
<li title="{{prop.id}}">
{{prop.name}}, {{prop.data_type}}
{%- if prop.default -%}
, default: "{{prop.default | print_typed_value}}"
{%- endif -%}
{%- if prop.optional %}, OPTIONAL{% endif %}
{%- if prop.multiple %}, MULTIPLE{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
<a href="/model/property/create/{{rel.model.id}}">Add a property</a>
</li>
{% endfor %}
</ul>
{% endif %}
<a href="/model/relation/create/{{model.id}}">Add a relation</a>
</li>
{% endfor %}
</ul>
{%- endblock %} {%- endblock %}

@ -0,0 +1,30 @@
{% extends "_layout" %}
{% block title -%}
Define object
{%- endblock %}
{% block nav -%}
<a href="/">Home</a>
{%- endblock %}
{% block content -%}
<h1>Define new object model</h1>
<form action="/model/object/create" method="POST">
<label for="parent">Parent:</label>
<select name="parent" id="parent">
<option value="">No parent</option>
{%- for model in all_models %}
<option value="{{model.id}}">{{model.name}}</option>
{%- endfor %}
</select><br>
<label for="name">Name:</label>
<input type="text" id="name" name="name"><br>
<input type="submit" value="Save">
</form>
{%- endblock %}

@ -0,0 +1,42 @@
{% extends "_layout" %}
{% block title -%}
Define property
{%- endblock %}
{% block nav -%}
<a href="/">Home</a>
{%- endblock %}
{% block content -%}
<h1>Add new property to model "{{model.name}}"</h1>
<form action="/model/property/create" method="POST">
<input type="hidden" name="object" value="{{object}}">
<label for="name">Name:</label>
<input type="text" id="name" name="name"><br>
<label for="optional">Optional:</label>
<input type="checkbox" name="optional" id="optional">
<br>
<label for="multiple">Multiple:</label>
<input type="checkbox" name="multiple" id="multiple"><br>
<label for="data_type">Type:</label>
<select name="data_type" id="data_type">
<option value="String" selected>String</option>
<option value="Integer">Integer</option>
<option value="Decimal">Decimal</option>
<option value="Boolean">Boolean</option>
</select><br>
<label for="default">Default:</label>
<input type="text" id="default" name="default"><br>
<input type="submit" value="Save">
</form>
{%- endblock %}

@ -1,5 +1,5 @@
#[macro_use] extern crate log; #[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 actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, HttpRequest};
use parking_lot::Mutex; use parking_lot::Mutex;
@ -13,8 +13,14 @@ use include_dir::Dir;
use std::sync::Arc; use std::sync::Arc;
use log::LevelFilter; use log::LevelFilter;
use crate::tera_ext::TeraExt; 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 tera_ext;
mod routes;
// Embed static files // Embed static files
include!(concat!(env!("OUT_DIR"), "/static_files.rs")); include!(concat!(env!("OUT_DIR"), "/static_files.rs"));
@ -22,35 +28,127 @@ include!(concat!(env!("OUT_DIR"), "/static_files.rs"));
// Embed templates // Embed templates
static TEMPLATES: include_dir::Dir = include_dir::include_dir!("./resources/templates"); static TEMPLATES: include_dir::Dir = include_dir::include_dir!("./resources/templates");
lazy_static! { pub(crate) static TERA : Lazy<Tera> = Lazy::new(|| {
static ref TERA : Tera = {
let mut tera = Tera::default(); let mut tera = Tera::default();
tera.add_include_dir_templates(&TEMPLATES); tera.add_include_dir_templates(&TEMPLATES);
tera
};
}
#[get("/")] // Special filter for the TypedValue map
async fn service_index(req: HttpRequest) -> actix_web::Result<impl Responder> { use serde_json::Value;
let mut context = tera::Context::new(); tera.register_filter("print_typed_value", |v : &Value, _ : &HashMap<String, Value>| -> tera::Result<Value> {
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| { tera
actix_web::error::ErrorInternalServerError(e) });
})?;
Ok(HttpResponse::Ok().body(html)) type YopaStoreWrapper = web::Data<tokio::sync::RwLock<yopa::Storage>>;
}
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
simple_logging::log_to_stderr(LevelFilter::Debug); 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 || { HttpServer::new(move || {
let static_files = actix_web_static_files::ResourceFiles::new("/static", included_static_files()) let static_files = actix_web_static_files::ResourceFiles::new("/static", included_static_files())
.do_not_resolve_defaults(); .do_not_resolve_defaults();
App::new() App::new()
.service(service_index) .app_data(database.clone())
.service(routes::index)
.service(static_files) .service(static_files)
}) })
.bind("127.0.0.1:8080")?.run().await .bind("127.0.0.1:8080")?.run().await

@ -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<RelationModelDisplay<'a>>,
}
#[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<impl Responder> {
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::<Vec<_>>();
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)
}

@ -1,5 +1,7 @@
use tera::Tera; use tera::Tera;
use include_dir::Dir; 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) { fn tera_includedir_walk_folder_inner(collected : &mut Vec<(String, String)>, tera : &mut Tera, dir: &Dir) {
for f in dir.files() { 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 { pub(crate) trait TeraExt {
fn add_include_dir_templates(&mut self, dir: &Dir) -> tera::Result<()>; fn add_include_dir_templates(&mut self, dir: &Dir) -> tera::Result<()>;
fn build_response(&self, template: &str, context : &tera::Context) -> actix_web::Result<HttpResponse> {
self.build_err_response(StatusCode::OK, template, context)
}
fn build_err_response(&self, code : StatusCode, template: &str, context : &tera::Context) -> actix_web::Result<HttpResponse>;
} }
impl TeraExt for Tera { impl TeraExt for Tera {
fn add_include_dir_templates(&mut self, dir: &Dir) -> tera::Result<()> { fn add_include_dir_templates(&mut self, dir: &Dir) -> tera::Result<()> {
debug!("Walk dir: {:?}", dir); debug!("Walk dir: {:?}", dir.path());
let mut templates = vec![]; let mut templates = vec![];
tera_includedir_walk_folder_inner(&mut templates, self, dir); tera_includedir_walk_folder_inner(&mut templates, self, dir);
self.add_raw_templates(templates) self.add_raw_templates(templates)
} }
fn build_err_response(&self, code : StatusCode, template: &str, context : &tera::Context) -> actix_web::Result<HttpResponse> {
let html = self.render(template, context).map_err(|e| {
actix_web::error::ErrorInternalServerError(e)
})?;
Ok(HttpResponse::build(code).body(html))
}
} }

@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize};
use crate::ID; use crate::ID;
use crate::model::DataType; use crate::model::DataType;
use crate::id::HaveId; use crate::id::HaveId;
use std::fmt::{Display, Formatter};
use std::fmt;
/// Value of a particular type /// Value of a particular type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -21,6 +23,17 @@ pub enum TypedValue {
Boolean(bool), 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 { impl TypedValue {
/// Try ot cast to another type. On error, the original value is returned as Err. /// Try ot cast to another type. On error, the original value is returned as Err.
pub fn cast_to(self, ty: DataType) -> Result<TypedValue, TypedValue> { pub fn cast_to(self, ty: DataType) -> Result<TypedValue, TypedValue> {

@ -13,6 +13,10 @@ use id::next_id;
use insert::InsertObj; use insert::InsertObj;
use insert::InsertValue; use insert::InsertValue;
use model::ObjectModel; use model::ObjectModel;
use crate::model::{PropertyModel, RelationModel};
pub use data::{TypedValue};
pub use model::{DataType};
pub mod model; pub mod model;
pub mod data; pub mod data;
@ -26,7 +30,7 @@ mod serde_map_as_list;
/// Stupid storage with no persistence /// Stupid storage with no persistence
#[derive(Debug, Default, Serialize, Deserialize, Clone)] #[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct InMemoryStorage { pub struct Storage {
#[serde(with = "serde_map_as_list")] #[serde(with = "serde_map_as_list")]
obj_models: HashMap<ID, model::ObjectModel>, obj_models: HashMap<ID, model::ObjectModel>,
#[serde(with = "serde_map_as_list")] #[serde(with = "serde_map_as_list")]
@ -51,7 +55,7 @@ pub enum StorageError {
ConstraintViolation(Cow<'static, str>), ConstraintViolation(Cow<'static, str>),
} }
impl InMemoryStorage { impl Storage {
/// Create empty store /// Create empty store
pub fn new() -> Self { pub fn new() -> Self {
Self::default() 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) { if let Some(x) = self.obj_models.get(&id) {
x.to_string() x.to_string()
} else if let Some(x) = self.rel_models.get(&id) { } 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 // DATA
/// Insert object with relations, validating the data model constraints /// 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) { 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 let Some(values) = values_by_id.remove(id) {
if values.len() > 1 && !prop.multiple { 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 { for val_instance in values {
values_to_insert.push(data::Value { values_to_insert.push(data::Value {
@ -238,7 +254,7 @@ impl InMemoryStorage {
value: def.clone(), value: def.clone(),
}); });
} else { } 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( return Err(StorageError::ConstraintViolation(
format!("{} of {} requires object of type {}, got {}", format!("{} of {} requires object of type {}, got {}",
relation_model, obj_model, relation_model, obj_model,
self.describe_id(relation_model.related), self.describe_model(relation_model.related),
self.describe_id(related.model)).into())); self.describe_model(related.model)).into()));
} }
} }
@ -301,4 +317,19 @@ impl InMemoryStorage {
Ok(object_id) Ok(object_id)
} }
// Reading
pub fn get_object_models(&self) -> impl Iterator<Item=&ObjectModel> {
self.obj_models.values()
}
pub fn get_grouped_prop_models(&self) -> HashMap<ID, Vec<&PropertyModel>> {
self.prop_models.values()
.into_group_map_by(|model| model.object)
}
pub fn get_grouped_relation_models(&self) -> HashMap<ID, Vec<&RelationModel>> {
self.rel_models.values()
.into_group_map_by(|model| model.object)
}
} }

Loading…
Cancel
Save