build form from yaml
This commit is contained in:
+28
-10
@@ -5,7 +5,7 @@
|
||||
|
||||
//use rocket::request::FromSegments;
|
||||
//use rocket::http::uri::Segments;
|
||||
//use rocket_contrib::serve::StaticFiles;
|
||||
use rocket_contrib::serve::StaticFiles;
|
||||
use rocket_contrib::templates::Template;
|
||||
|
||||
mod store;
|
||||
@@ -16,24 +16,42 @@ use rocket::response::Redirect;
|
||||
use rocket::http::Status;
|
||||
use rocket::request::Form;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use crate::store::form::RenderedField;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FormContext<'a> {
|
||||
pub fields : Vec<RenderedField<'a>>,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn index(store : State<RwLock<Store>>) -> Template {
|
||||
let mut context = HashMap::new();
|
||||
let rg = store.read();
|
||||
context.insert("records", &rg.parts);
|
||||
|
||||
let indexes = &rg.index;
|
||||
let context = FormContext {
|
||||
fields: rg.model.fields.iter().map(|(key, field)| {
|
||||
RenderedField::from_template_field(key, field, None, indexes)
|
||||
}).collect()
|
||||
};
|
||||
|
||||
Template::render("index", context)
|
||||
}
|
||||
|
||||
#[post("/add", data="<record>")]
|
||||
fn add_part(store : State<RwLock<Store>>, record : Form<store::Part>) -> Redirect {
|
||||
store.write().add(record.into_inner());
|
||||
Redirect::to(uri!(index))
|
||||
}
|
||||
//#[post("/add", data="<record>")]
|
||||
//fn add_part(store : State<RwLock<Store>>, record : Form<store::Part>) -> Redirect {
|
||||
// store.write().add(record.into_inner());
|
||||
// Redirect::to(uri!(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::new()))
|
||||
.mount("/", routes![index, add_part]).launch();
|
||||
.manage(RwLock::new(store))
|
||||
.mount("/", StaticFiles::from(cwd.join("templates/static/")))
|
||||
.mount("/", routes![index]).launch();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
use crate::store::model::FieldKind;
|
||||
use serde_json::Value;
|
||||
use std::borrow::Cow;
|
||||
use crate::store::{model, Indexes};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
/// This is an example for using doc comment attributes
|
||||
static ref EMPTY_VEC: Vec<String> = vec![];
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Default)]
|
||||
pub struct RenderedField<'a> {
|
||||
pub key: Cow<'a, str>,
|
||||
pub label: Cow<'a, str>,
|
||||
pub kind: &'static str,
|
||||
pub step: &'static str,
|
||||
pub min: String,
|
||||
pub max: String,
|
||||
pub options: Option<&'a Vec<String>>,
|
||||
pub value: Cow<'a, str>,
|
||||
pub checked: bool,
|
||||
}
|
||||
|
||||
impl<'a> RenderedField<'a> {
|
||||
pub fn from_template_field<'i>(
|
||||
key: &'i String,
|
||||
field: &'i model::Field,
|
||||
value: Option<&'i Value>,
|
||||
index: &'i Indexes
|
||||
) -> RenderedField<'i> {
|
||||
let mut rendered = RenderedField::default();
|
||||
rendered.key = key.as_str().into();
|
||||
rendered.label = if field.label.is_empty() {
|
||||
rendered.key.clone()
|
||||
} else {
|
||||
field.label.as_str().into()
|
||||
};
|
||||
|
||||
match &field.kind {
|
||||
FieldKind::String => {
|
||||
rendered.kind = "string";
|
||||
|
||||
if let Some(Value::String(s)) = value {
|
||||
rendered.value = Cow::Borrowed(&s.as_str());
|
||||
}
|
||||
}
|
||||
FieldKind::Text => {
|
||||
rendered.kind = "text";
|
||||
|
||||
if let Some(Value::String(s)) = value {
|
||||
rendered.value = Cow::Borrowed(&s.as_str());
|
||||
}
|
||||
}
|
||||
FieldKind::Bool { default } => {
|
||||
rendered.kind = "bool";
|
||||
|
||||
rendered.checked = if let Some(Value::Bool(v)) = value {
|
||||
*v
|
||||
} else {
|
||||
*default
|
||||
}
|
||||
}
|
||||
FieldKind::Int { min, max, default } => {
|
||||
rendered.kind = "number";
|
||||
|
||||
let num = if let Some(Value::Number(n)) = value {
|
||||
n.as_i64().expect("Error parsing number")
|
||||
} else {
|
||||
*default
|
||||
};
|
||||
|
||||
if let Some(n) = min {
|
||||
rendered.min = n.to_string();
|
||||
}
|
||||
|
||||
if let Some(n) = max {
|
||||
rendered.max = n.to_string();
|
||||
}
|
||||
|
||||
rendered.value = Cow::Owned(num.to_string());
|
||||
rendered.step = "1";
|
||||
}
|
||||
FieldKind::Float { min, max, default } => {
|
||||
rendered.kind = "number";
|
||||
|
||||
let num = if let Some(Value::Number(n)) = value {
|
||||
n.as_f64().expect("Error parsing number")
|
||||
} else {
|
||||
*default
|
||||
};
|
||||
|
||||
if let Some(n) = min {
|
||||
rendered.min = n.to_string();
|
||||
}
|
||||
|
||||
if let Some(n) = max {
|
||||
rendered.max = n.to_string();
|
||||
}
|
||||
|
||||
rendered.value = Cow::Owned(num.to_string());
|
||||
rendered.step = "any";
|
||||
}
|
||||
FieldKind::Enum { options, default } => {
|
||||
rendered.kind = "select";
|
||||
rendered.options = Some(options);
|
||||
}
|
||||
FieldKind::FreeEnum { enum_group } => {
|
||||
rendered.kind = "free_select";
|
||||
let group = enum_group.as_ref().unwrap_or(key);
|
||||
rendered.options = Some(index.free_enums.get(group).unwrap_or(&EMPTY_VEC))
|
||||
}
|
||||
FieldKind::Tags { options } => {
|
||||
rendered.kind = "select";
|
||||
rendered.options = Some(options);
|
||||
}
|
||||
FieldKind::FreeTags { tag_group } => {
|
||||
rendered.kind = "free_select";
|
||||
let group = tag_group.as_ref().unwrap_or(key);
|
||||
rendered.options = Some(index.free_tags.get(group).unwrap_or(&EMPTY_VEC))
|
||||
}
|
||||
}
|
||||
|
||||
rendered
|
||||
}
|
||||
}
|
||||
+69
-33
@@ -1,52 +1,88 @@
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Write, Error};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use rocket::request::FromForm;
|
||||
use crate::store::model::Model;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize,Deserialize)]
|
||||
pub mod model;
|
||||
pub mod form;
|
||||
|
||||
/// Store instance
|
||||
#[derive(Debug)]
|
||||
pub struct Store {
|
||||
pub parts : Vec<Part>
|
||||
path : PathBuf,
|
||||
pub model: Model,
|
||||
pub items : HashMap<usize, serde_json::Value>,
|
||||
pub index : Indexes,
|
||||
}
|
||||
|
||||
#[derive(Serialize,Deserialize,FromForm)]
|
||||
pub struct Part {
|
||||
name : String,
|
||||
quantity : usize,
|
||||
location : String,
|
||||
/// Indexes loaded from the indexes file
|
||||
#[derive(Serialize,Deserialize,Debug,Default)]
|
||||
pub struct Indexes {
|
||||
pub free_enums : HashMap<String, Vec<String>>,
|
||||
pub free_tags : HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
fn load_file_or(file : impl AsRef<Path>, def : String) -> String {
|
||||
/// Struct loaded from the repositroy config file
|
||||
#[derive(Serialize,Deserialize,Debug)]
|
||||
struct RepositoryConfig {
|
||||
pub model : Model,
|
||||
}
|
||||
|
||||
const REPO_CONFIG_FILE : &'static str = "repository.yaml";
|
||||
const REPO_DATA_FILE : &'static str = "data.json";
|
||||
const REPO_INDEX_FILE : &'static str = "index.json";
|
||||
|
||||
impl Store {
|
||||
pub fn new(path: impl AsRef<Path>) -> Self {
|
||||
let file = load_file(path.as_ref().join(REPO_CONFIG_FILE));
|
||||
|
||||
let repository_config : RepositoryConfig = serde_yaml::from_str(&file)
|
||||
.expect("Error parsing repository config file.");
|
||||
|
||||
let items = load_file_or(path.as_ref().join(REPO_DATA_FILE), "{}");
|
||||
let indexes = load_file_or(path.as_ref().join(REPO_INDEX_FILE), "{}");
|
||||
|
||||
Store {
|
||||
path: path.as_ref().into(),
|
||||
model: repository_config.model,
|
||||
items: serde_json::from_str(&items).expect("Error parsing data file."),
|
||||
index: serde_json::from_str(&indexes).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn persist(&self) {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(self.path.join(REPO_DATA_FILE))
|
||||
.expect("Error opening data file for writing.");
|
||||
|
||||
let serialized = serde_json::to_string(&self.items).expect("Error serialize.");
|
||||
file.write(serialized.as_bytes()).expect("Error write data file");
|
||||
}
|
||||
}
|
||||
|
||||
fn load_file(path: impl AsRef<Path>) -> String {
|
||||
let mut file= File::open(&path).expect(&format!("Error opening file {}", path.as_ref().display()));
|
||||
|
||||
let mut buf = String::new();
|
||||
file.read_to_string(&mut buf).expect(&format!("Error reading file {}", path.as_ref().display()));
|
||||
buf
|
||||
}
|
||||
|
||||
fn load_file_or(file : impl AsRef<Path>, def : impl Into<String>) -> String {
|
||||
let mut file = match File::open(file) {
|
||||
Ok(file) => file,
|
||||
Err(_) => return def
|
||||
Err(_) => return def.into()
|
||||
};
|
||||
|
||||
let mut buf = String::new();
|
||||
if file.read_to_string(&mut buf).is_err() {
|
||||
return def;
|
||||
return def.into();
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn new() -> Self {
|
||||
let mut file = load_file_or("inventory.json", "[]".to_string());
|
||||
|
||||
Store {
|
||||
parts: serde_json::from_str(&file).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, part : Part) {
|
||||
self.parts.push(part);
|
||||
|
||||
self.persist()
|
||||
}
|
||||
|
||||
pub fn persist(&self) {
|
||||
let mut file = OpenOptions::new().write(true).create(true).truncate(true).open("inventory.json").unwrap();
|
||||
|
||||
file.write(serde_json::to_string(&self.parts).unwrap().as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A data card's model.
|
||||
/// Cards of one model can be sorted, searched and filtered by their fields.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Model {
|
||||
/// Fields defined by this model
|
||||
pub fields: HashMap<String, Field>,
|
||||
}
|
||||
|
||||
/// One field of a model
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Field {
|
||||
/// Field label shown in the user interface
|
||||
#[serde(default)]
|
||||
pub label: String,
|
||||
/// Field data type
|
||||
#[serde(flatten)]
|
||||
pub kind: FieldKind,
|
||||
}
|
||||
|
||||
/// Field data type and validations
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FieldKind {
|
||||
/// Single-line text entry
|
||||
String,
|
||||
|
||||
/// Long-form text entry, can have multiple rows
|
||||
Text,
|
||||
|
||||
/// Checkbox or a toggle switch
|
||||
Bool {
|
||||
/// Default value when the model's card is created
|
||||
#[serde(default)]
|
||||
default: bool,
|
||||
},
|
||||
|
||||
/// Integer entry
|
||||
Int {
|
||||
/// Lowest allowed value
|
||||
#[serde(default)]
|
||||
min: Option<i64>,
|
||||
/// Highest allowed value
|
||||
#[serde(default)]
|
||||
max: Option<i64>,
|
||||
/// Default value
|
||||
#[serde(default)]
|
||||
default: i64,
|
||||
},
|
||||
|
||||
/// Floating point entry
|
||||
Float {
|
||||
/// Lowest allowed value
|
||||
#[serde(default)]
|
||||
min: Option<f64>,
|
||||
/// Highest allowed value
|
||||
#[serde(default)]
|
||||
max: Option<f64>,
|
||||
/// Default value
|
||||
#[serde(default)]
|
||||
default: f64,
|
||||
},
|
||||
|
||||
/// Enumeration entry with a fixed set of options
|
||||
Enum {
|
||||
/// Options to choose from, must not be empty
|
||||
options: Vec<String>,
|
||||
/// Default option (if not the first)
|
||||
#[serde(default)]
|
||||
default: Option<String>,
|
||||
},
|
||||
|
||||
/// Enum that can be freely expanded by the user
|
||||
FreeEnum {
|
||||
/// Group name.
|
||||
/// If not set, a private group for this particular field is used.
|
||||
#[serde(default)]
|
||||
enum_group: Option<String>,
|
||||
},
|
||||
|
||||
/// Tags with a fixed set of options to choose from
|
||||
Tags {
|
||||
/// Options to choose from
|
||||
options: Vec<String>,
|
||||
},
|
||||
|
||||
/// Tags that can be freely added by the user
|
||||
FreeTags {
|
||||
/// Group name.
|
||||
/// If not set, a private group for this particular field is used.
|
||||
#[serde(default)]
|
||||
tag_group: Option<String>,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user