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/routes.rs

419 lines
13 KiB

use actix_web::{web, HttpRequest, Responder, HttpResponse};
use crate::TERA;
use crate::tera_ext::TeraExt;
use yopa::{Storage, StorageError, ID, DataType, TypedValue};
use serde::{Deserialize, Serialize};
use yopa::model::{PropertyModel, RelationModel, ObjectModel};
use std::ops::DerefMut;
use actix_session::Session;
use crate::session_ext::SessionExt;
use std::str::FromStr;
use std::fmt::{Debug, Display};
use actix_web::http::header::IntoHeaderValue;
#[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>,
}
fn redirect(path : impl IntoHeaderValue) -> actix_web::Result<HttpResponse> {
Ok(HttpResponse::SeeOther()
.header("location", path) // back - to where?
.finish())
}
trait ParseOrBadReq {
fn parse_or_bad_request<T, E>(&self) -> actix_web::Result<T>
where T: FromStr<Err=E>,
E: Display + Debug + 'static;
}
impl ParseOrBadReq for &str {
fn parse_or_bad_request<T, E>(&self) -> actix_web::Result<T>
where T: FromStr<Err=E>,
E: Display + Debug + 'static
{
self.parse::<T>()
.map_err(|e| {
error!("Parse error for \"{}\"", self);
actix_web::error::ErrorBadRequest(e)
})
}
}
impl ParseOrBadReq for String {
fn parse_or_bad_request<T, E>(&self) -> actix_web::Result<T>
where T: FromStr<Err=E>,
E: Display + Debug + 'static
{
self.as_str()
.parse_or_bad_request()
}
}
#[get("/")]
pub(crate) async fn index(session : Session, 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);
session.render_flash(&mut ctx);
TERA.build_response("index", &ctx)
}
#[get("/model/object/create")]
pub(crate) async fn object_model_create_form(session : Session) -> actix_web::Result<impl Responder> {
let mut context = tera::Context::new();
session.render_flash(&mut context);
TERA.build_response("model_create", &context)
}
#[derive(Deserialize)]
pub(crate) struct ObjectModelCreate {
pub name : String,
}
#[post("/model/object/create")]
pub(crate) async fn object_model_create(
form : web::Form<ObjectModelCreate>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
let form = form.into_inner();
match wg.define_object(ObjectModel {
id: Default::default(),
name: form.name.clone()
}) {
Ok(_id) => {
debug!("Object created, redirecting to root");
session.flash_success(format!("Object model \"{}\" created.", form.name));
redirect("/")
}
Err(e) => {
warn!("Error creating model: {:?}", e);
session.flash_error(e.to_string());
redirect("/model/object/create")
}
}
}
#[get("/model/relation/create/{object_id}")]
pub(crate) async fn relation_model_create_form(
object_id : web::Path<String>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut context = tera::Context::new();
session.render_flash(&mut context);
let rg = store.read().await;
debug!("ID = {}", object_id);
let object = rg.get_object_model(object_id.parse_or_bad_request()?)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such source object"))?;
let mut models: Vec<_> = rg.get_object_models().collect();
models.sort_by_key(|m| &m.name);
context.insert("models", &models);
context.insert("object", &object);
TERA.build_response("relation_create", &context)
}
#[derive(Deserialize)]
pub(crate) struct RelationModelCreate {
pub object : ID,
pub name : String,
pub reciprocal_name : String,
pub optional : Option<i32>,
pub multiple : Option<i32>,
pub related : ID,
}
#[post("/model/relation/create")]
pub(crate) async fn relation_model_create(
form : web::Form<RelationModelCreate>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
let form = form.into_inner();
match wg.define_relation(RelationModel {
id: Default::default(),
object: form.object,
name: form.name.clone(),
reciprocal_name: form.reciprocal_name.clone(),
optional: form.optional.unwrap_or_default() != 0,
multiple: form.multiple.unwrap_or_default() != 0,
related: form.related
}) {
Ok(_id) => {
debug!("Relation created, redirecting to root");
session.flash_success(format!("Relation model \"{}\" created.", form.name));
redirect("/")
}
Err(e) => {
warn!("Error creating relation model: {:?}", e);
session.flash_error(e.to_string());
redirect(format!("/model/relation/create/{}", form.object))
}
}
}
#[derive(Serialize, Debug)]
struct ObjectOrRelationModelDisplay {
id : ID,
describe : String,
}
#[get("/model/property/create/{object_id}")]
pub(crate) async fn property_model_create_form(
object_id : web::Path<String>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut context = tera::Context::new();
session.render_flash(&mut context);
let rg = store.read().await;
debug!("ID = {}", object_id);
let object = {
let id = object_id.parse_or_bad_request()?;
debug!("Create property for ID={}", id);
if let Some(om) = rg.get_object_model(id) {
ObjectOrRelationModelDisplay {
id: om.id,
describe: format!("object model \"{}\"", om.name),
}
} else if let Some(rm) = rg.get_relation_model(id) {
ObjectOrRelationModelDisplay {
id: rm.id,
describe: format!("relation model \"{}\"", rm.name),
}
} else {
return Err(actix_web::error::ErrorNotFound("No such source object"));
}
};
context.insert("object", &object);
TERA.build_response("property_create", &context)
}
#[derive(Deserialize)]
pub(crate) struct PropertyModelCreate {
pub object : ID,
pub name : String,
pub optional : Option<i32>,
pub multiple : Option<i32>,
pub data_type : DataType,
/// Default value to be parsed to the data type
/// May be unused if empty and optional
pub default : String,
}
#[post("/model/property/create")]
pub(crate) async fn property_model_create(
form : web::Form<PropertyModelCreate>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
let form = form.into_inner();
let optional = form.optional.unwrap_or_default() != 0;
let multiple = form.multiple.unwrap_or_default() != 0;
match wg.define_property(PropertyModel {
id: Default::default(),
object: form.object,
name: form.name.clone(),
optional,
multiple,
data_type: form.data_type,
default: {
match form.data_type {
DataType::String => {
if form.default.is_empty() && optional {
None
} else {
Some(TypedValue::String(form.default.into()))
}
}
DataType::Integer => {
if form.default.is_empty() {
if optional {
None
} else {
Some(TypedValue::Integer(0))
}
} else {
// TODO better error reporting
Some(TypedValue::Integer(form.default.parse()
.map_err(|_| {
actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as integer", form.default))
})?))
}
}
DataType::Decimal => {
if form.default.is_empty() {
if optional {
None
} else {
Some(TypedValue::Decimal(0.0))
}
} else {
// TODO better error reporting
Some(TypedValue::Decimal(form.default.parse()
.map_err(|_| {
actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as decimal", form.default))
})?))
}
}
DataType::Boolean => {
if form.default.is_empty() {
if optional {
None
} else {
Some(TypedValue::Boolean(false))
}
} else {
Some(TypedValue::String(form.default.clone().into())
.cast_to(DataType::Boolean).map_err(|_| {
actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as boolean", form.default))
})?)
}
}
}
}
}) {
Ok(_id) => {
debug!("Property created, redirecting to root");
session.flash_success(format!("Property model \"{}\" created.", form.name));
redirect("/")
}
Err(e) => {
warn!("Error creating property model: {:?}", e);
session.flash_error(e.to_string());
redirect(format!("/model/property/create/{}", form.object))
}
}
}
#[get("/model/object/delete/{id}")]
pub(crate) async fn object_model_delete(
id : web::Path<String>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
match wg.undefine_object(id.parse().map_err(|e| actix_web::error::ErrorBadRequest(e))?) {
Ok(om) => {
debug!("Object model deleted, redirecting to root");
session.flash_success(format!("Object model \"{}\" deleted.", om.name));
redirect("/")
}
Err(e) => {
warn!("Error deleting object model: {:?}", e);
session.flash_error(e.to_string());
redirect("/") // back?
}
}
}
#[get("/model/relation/delete/{id}")]
pub(crate) async fn relation_model_delete(
id : web::Path<String>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
match wg.undefine_relation(id.parse_or_bad_request()?) {
Ok(rm) => {
debug!("Relation deleted, redirecting to root");
session.flash_success(format!("Relation model \"{}\" deleted.", rm.name));
redirect("/")
}
Err(e) => {
warn!("Error deleting relation model: {:?}", e);
session.flash_error(e.to_string());
redirect("/") // back?
}
}
}
#[get("/model/property/delete/{id}")]
pub(crate) async fn property_model_delete(
id : web::Path<String>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
match wg.undefine_property(id.parse_or_bad_request()?) {
Ok(rm) => {
debug!("Property deleted, redirecting to root");
session.flash_success(format!("Property \"{}\" deleted.", rm.name));
redirect("/")
}
Err(e) => {
warn!("Error deleting property: {:?}", e);
session.flash_error(e.to_string());
redirect("/") // back?
}
}
}