build form from yaml

This commit is contained in:
2019-12-29 22:23:39 +01:00
parent a1b8f5a1a1
commit faa41feaf6
12 changed files with 575 additions and 56 deletions
+28 -10
View File
@@ -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();
}
+127
View File
@@ -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
View File
@@ -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();
}
}
+96
View File
@@ -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>,
},
}