|
|
|
#[macro_use] extern crate log;
|
|
|
|
#[macro_use] extern crate actix_web;
|
|
|
|
|
|
|
|
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::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
use std::path::{PathBuf, Path};
|
|
|
|
use actix_web_static_files;
|
|
|
|
use tera::Tera;
|
|
|
|
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 rand::Rng;
|
|
|
|
use actix_web_static_files::ResourceFiles as StaticFiles;
|
|
|
|
|
|
|
|
mod tera_ext;
|
|
|
|
mod routes;
|
|
|
|
mod session_ext;
|
|
|
|
|
|
|
|
// 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"))
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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::object_model_create_form)
|
|
|
|
.service(routes::object_model_create)
|
|
|
|
.service(static_files)
|
|
|
|
.default_service(web::to(|| HttpResponse::NotFound().body("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();
|
|
|
|
|
|
|
|
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))
|
|
|
|
}
|