Flat file database editor and browser with web interface
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.
 
 
rocket-inv/src/main.rs

248 lines
7.1 KiB

#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate serde;
use serde_json::{json, Value, Number};
//use rocket::request::FromSegments;
//use rocket::http::uri::Segments;
use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template;
mod store;
use crate::store::Store;
use rocket::{State, Data};
use parking_lot::RwLock;
use rocket::response::Redirect;
use rocket::http::Status;
use rocket::request::{Form, LenientForm, FromForm, FormItems};
use std::env;
use crate::store::form::{RenderedField, render_empty_fields, RenderedCard, render_card_fields};
use crate::store::model::FieldKind;
use std::ops::Deref;
use indexmap::map::IndexMap;
#[derive(Serialize, Debug)]
pub struct ListContext<'a> {
pub fields: Vec<RenderedField<'a>>,
pub cards: Vec<RenderedCard<'a>>,
}
#[get("/")]
fn route_index(store: State<RwLock<Store>>) -> Template {
let rg = store.read();
let context = ListContext {
fields: render_empty_fields(&rg),
cards: rg.data.cards.iter().filter_map(|(id, card)| {
if let Value::Object(map) = card {
Some(RenderedCard {
fields: render_card_fields(&rg, map),
id: *id,
})
} else {
None
}
}).collect(),
};
Template::render("index", context)
}
#[derive(Default)]
struct MapFromForm {
pub data: IndexMap<String, String>
}
impl<'a> FromForm<'a> for MapFromForm {
type Error = ();
fn from_form(items: &mut FormItems, _strict: bool) -> Result<Self, Self::Error> {
let mut new = MapFromForm::default();
items.for_each(|item| {
let (k, v) = item.key_value_decoded();
new.data.insert(k, v);
});
Ok(new)
}
}
fn collect_card_form(store : &Store, mut form : MapFromForm) -> IndexMap::<String, Value> {
let mut card = IndexMap::new();
for (k, field) in &store.model.fields {
let mut value: Option<Value> = None;
if let Some(input) = form.data.remove(k) {
value = Some(match &field.kind {
FieldKind::Text | FieldKind::String | FieldKind::FreeEnum { .. } => {
Value::String(input)
}
FieldKind::Bool { .. } => {
serde_json::to_value(true).unwrap()
}
FieldKind::Int { min, max, default } => {
let mut val: i64 = if input.is_empty() {
*default
} else {
input.parse().expect("Error parse number")
};
if let Some(min) = min {
val = val.max(*min);
}
if let Some(max) = max {
val = val.min(*max);
}
serde_json::to_value(val).unwrap()
}
FieldKind::Float { min, max, default } => {
let mut val: f64 = if input.is_empty() {
*default
} else {
input.parse().expect("Error parse number")
};
if let Some(min) = min {
val = val.min(*min);
}
if let Some(max) = max {
val = val.max(*max);
}
serde_json::to_value(val).unwrap()
}
FieldKind::Enum { options, default } => {
if options.contains(&input) {
Value::String(input)
} else {
let val = default
.as_ref()
.map(ToOwned::to_owned)
.unwrap_or_else(|| options
.first()
.expect("fixed enum must have values")
.to_owned()
);
Value::String(val)
}
}
FieldKind::Tags { options } => {
let tags: Vec<String> = input.split(' ')
.map(ToOwned::to_owned)
.filter_map(|tag| {
if options.contains(&tag) {
Some(tag)
} else {
None
}
}).collect();
serde_json::to_value(tags).unwrap()
}
FieldKind::FreeTags { .. } => {
let tags: Vec<String> = input.split(' ')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(ToOwned::to_owned)
.collect();
serde_json::to_value(tags).unwrap()
}
});
} else {
if let FieldKind::Bool { .. } = field.kind {
value = Some(Value::Bool(false));
}
}
if let Some(v) = value {
card.insert(k.to_owned(), v);
}
}
card
}
#[derive(Serialize)]
struct AddCardContext<'a> {
pub fields: Vec<RenderedField<'a>>,
}
#[derive(Serialize)]
struct EditCardContext<'a> {
pub fields: Vec<RenderedField<'a>>,
pub id : usize,
}
#[get("/add")]
fn route_add(store: State<RwLock<Store>>) -> Template {
let rg = store.read();
let context = AddCardContext {
fields: render_empty_fields(&rg)
};
Template::render("add", context)
}
#[post("/add", data = "<form>")]
fn route_add_save(form: Form<MapFromForm>, store: State<RwLock<Store>>) -> Redirect {
let mut rg = store.write();
let card = collect_card_form(&rg, form.into_inner());
rg.add_card(card);
Redirect::found(uri!(route_index))
}
#[get("/edit/<id>")]
fn route_edit(id : usize, store: State<RwLock<Store>>) -> Option<Template> {
let rg = store.read();
if let Some(Value::Object(map)) = rg.data.cards.get(&id) {
let context = EditCardContext {
fields: render_card_fields(&rg, map),
id,
};
Some(Template::render("edit", context))
} else {
None
}
}
#[post("/edit/<id>", data = "<form>")]
fn route_edit_save(id : usize, form: Form<MapFromForm>, store: State<RwLock<Store>>) -> Option<Redirect> {
let mut rg = store.write();
let card = collect_card_form(&rg, form.into_inner());
rg.update_card(id, card);
Some(Redirect::found(uri!(route_index)))
}
fn main() {
let cwd = env::current_dir().unwrap();
let data_dir = cwd.join("data");
let store = Store::new(data_dir);
rocket::ignite()
.attach(Template::fairing())
.manage(RwLock::new(store))
.mount("/", StaticFiles::from(cwd.join("templates/static/")))
.mount("/", routes![
route_index,
route_add,
route_add_save,
route_edit,
route_edit_save,
]).launch();
}