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:
+
+
+{% for model in models %}
+ -
+ {{model.name}}
+
+ {#
+
+ {% if !model.properties.is_empty() %}
+ Properties:
+
+ {% for prop in model.properties %}
+ - {{prop.name}}, {{prop.data_type}}, def: {{prop.default}}, opt: {{prop.optional}}, mult: {{prop.multiple}}
+ {% endfor %}
+
+ {% endif %}
+
+ New property
+
+ {% if !model.relations.is_empty() %}
+ Relations:
+
+ {% for rel in model.relations %}
+ -
+ {{rel.name}} -> {{rel.related_name}}
+
+ {% if !rel.properties.is_empty() %}
+ Properties:
+
+ {% for prop in rel.properties %}
+ - {{prop.name}}, {{prop.data_type}}, def: {{prop.default}}, opt: {{prop.optional}}, mult: {{prop.multiple}}
+ {% endfor %}
+
+ {% endif %}
+
+ {% endfor %}
+
+ {% endif %}
+
+ New relation
+
+ #}
+
+{% endfor %}
+
+
+{%- 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:
+
+
+{% for model in models %}
+ -
+ {{model.name}}
+
+ {% if model.properties %}
+ Properties:
+
+ {% for prop in model.properties %}
+ -
+ {{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 %}
+
+ {% endfor %}
+
+ {% endif %}
+
+ Add a property
+
+ {% if model.relations %}
+ Relations:
+
+ {% for rel in model.relations %}
+ -
+ "{{rel.model.name}}", pointing to: {{rel.related_name}}
+
+ {% if rel.properties %}
+ Properties:
+
+ {% for prop in rel.properties %}
+ -
+ {{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 %}
+
+ {% endfor %}
+
+ {% endif %}
+
+ Add a property
+
+ {% endfor %}
+
+ {% endif %}
+
+ Add a relation
+
+{% endfor %}
+
{%- 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)
+ }
}