a small relational database with user-editable schema for manual data entry
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
yopa/yopa-web/src/main.rs

296 lines
8.7 KiB

#[macro_use]
extern crate actix_web;
#[macro_use]
extern crate log;
use std::collections::HashMap;
use std::ops::Deref;
use actix_session::CookieSession;
use actix_web::{web, App, HttpResponse, HttpServer};
use actix_web_static_files;
use actix_web_static_files::ResourceFiles as StaticFiles;
use log::LevelFilter;
use once_cell::sync::Lazy;
use rand::Rng;
use tera::Tera;
use yopa::insert::{InsertObj, InsertRel, InsertValue};
use yopa::{Storage, TypedValue};
use crate::tera_ext::TeraExt;
mod routes;
mod session_ext;
mod tera_ext;
mod utils;
// Embed static files
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<Tera> = 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<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"))
},
);
// opt(checked=foo.is_checked)
tera.register_function(
"opt",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
if args.len() != 1 {
return Err("Expected 1 argument".into());
}
match args.iter().nth(0) {
Some((name, &Value::Bool(true))) => Ok(Value::String(name.clone())),
Some((_, &Value::Bool(false))) => Ok(Value::Null),
_ => Err("Expected bool argument".into()),
}
},
);
// selected(val=foo.color,opt="Red")
tera.register_function(
"selected",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
match (args.get("val"), args.get("opt")) {
(Some(v), Some(w)) => {
if v == w {
Ok(Value::String("selected".into()))
} else {
Ok(Value::Null)
}
}
_ => Err("Expected val and opt args".into()),
}
},
);
// TODO need to inject HttpRequest::url_for() into tera context, but it then can't be accessed by the functions.
// tera.register_function("url_for", |args: HashMap<String, Value>| -> tera::Result<Value> {
// match args.get("name") {
// Some(Value::String(s)) => {
// let r =
// },
// _ => Err("Expected string argument".into()),
// }
// });
tera
});
type YopaStoreWrapper = web::Data<tokio::sync::RwLock<yopa::Storage>>;
#[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 yopa_store: YopaStoreWrapper = init_yopa();
let mut session_key = [0u8; 32];
rand::thread_rng().fill(&mut session_key);
debug!("Session key: {:?}", session_key);
HttpServer::new(move || {
let static_files =
StaticFiles::new("/static", included_static_files()).do_not_resolve_defaults();
App::new()
/* Middlewares */
.wrap(CookieSession::signed(&session_key).secure(false))
/* Bind shared objects */
.app_data(yopa_store.clone())
/* Routes */
.service(routes::index)
.service(routes::takeout)
//
.service(routes::models::list)
//
.service(routes::models::object::create_form)
.service(routes::models::object::create)
.service(routes::models::object::update_form)
.service(routes::models::object::update)
.service(routes::models::object::delete)
//
.service(routes::models::relation::create_form)
.service(routes::models::relation::create)
.service(routes::models::relation::update_form)
.service(routes::models::relation::update)
.service(routes::models::relation::delete)
//
.service(routes::models::property::create_form)
.service(routes::models::property::create)
.service(routes::models::property::update_form)
.service(routes::models::property::update)
.service(routes::models::property::delete)
//
.service(routes::objects::list)
.service(routes::objects::create_form)
.service(routes::objects::create)
.service(routes::objects::detail)
.service(routes::objects::update_form)
.service(routes::objects::update)
.service(routes::objects::delete)
//
.service(static_files)
.default_service(web::to(|| {
HttpResponse::NotFound().body("File or endpoint not found")
}))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
fn init_yopa() -> 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(),
})
.unwrap();
let id_book = store
.define_object(model::ObjectModel {
id: Default::default(),
name: "Book".to_string(),
})
.unwrap();
let _id_ing = store
.define_object(model::ObjectModel {
id: Default::default(),
name: "Ingredient".to_string(),
})
.unwrap();
let val_descr = store
.define_property(model::PropertyModel {
id: Default::default(),
object: id_recipe,
name: "description".to_string(),
optional: true,
multiple: true,
data_type: DataType::String,
default: TypedValue::String("".into()),
})
.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: 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(),
reciprocal_name: "recipes".to_string(),
optional: true,
multiple: true,
related: id_book,
})
.unwrap();
let page = 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: TypedValue::Integer(0),
})
.unwrap();
store
.define_relation(model::RelationModel {
id: Default::default(),
object: id_recipe,
name: "related recipe".to_string(),
reciprocal_name: "related recipe".to_string(),
optional: true,
multiple: true,
related: id_recipe,
})
.unwrap();
let book1 = store
.insert_object(InsertObj {
model: id_book,
name: "Book 1".to_string(),
values: vec![],
relations: vec![],
})
.unwrap();
store
.insert_object(InsertObj {
model: id_book,
name: "Book 2".to_string(),
values: vec![],
relations: vec![],
})
.unwrap();
store
.insert_object(InsertObj {
model: id_recipe,
name: "Recipe1".to_string(),
values: vec![InsertValue {
model: val_descr,
value: TypedValue::String("Bla bla bla".into()),
}],
relations: vec![InsertRel {
model: rel_book_id,
related: book1,
values: vec![InsertValue {
model: page,
value: TypedValue::Integer(15),
}],
}],
})
.unwrap();
*/
web::Data::new(tokio::sync::RwLock::new(store))
}