From faa41feaf6e3f6d689210013793e5beef7e34c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 29 Dec 2019 22:23:39 +0100 Subject: [PATCH] build form from yaml --- .gitignore | 2 +- Cargo.lock | 71 ++++++++++++++++++ Cargo.toml | 3 + data/repository.yaml | 32 ++++++++ src/main.rs | 38 +++++++--- src/store/form.rs | 127 ++++++++++++++++++++++++++++++++ src/store/mod.rs | 94 +++++++++++++++-------- src/store/model.rs | 96 ++++++++++++++++++++++++ templates/form_macros.html.tera | 67 +++++++++++++++++ templates/index.html.tera | 55 +++++++++++--- templates/layout.html.tera | 11 +++ templates/static/style.css | 27 +++++++ 12 files changed, 571 insertions(+), 52 deletions(-) create mode 100644 data/repository.yaml create mode 100644 src/store/form.rs create mode 100644 src/store/model.rs create mode 100644 templates/form_macros.html.tera create mode 100644 templates/layout.html.tera create mode 100644 templates/static/style.css diff --git a/.gitignore b/.gitignore index ed70c7f..60d1ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target **/*.rs.bk .idea/ -inventory.json + diff --git a/Cargo.lock b/Cargo.lock index 1dd7a13..e3ebbe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,6 +174,11 @@ dependencies = [ "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "dtoa" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "educe" version = "0.4.2" @@ -304,6 +309,28 @@ dependencies = [ "unicode-normalization 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ifmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ifmt-impl 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ifmt-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "indexmap" version = "1.3.0" @@ -382,6 +409,11 @@ name = "libc" version = "0.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "linked-hash-map" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "lock_api" version = "0.3.2" @@ -633,6 +665,16 @@ dependencies = [ "sha-1 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "proc-macro2" version = "0.4.30" @@ -733,13 +775,16 @@ dependencies = [ name = "rocket-inv" version = "0.1.0" dependencies = [ + "ifmt 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "json_dotpath 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "rocket 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "rocket-download-response 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "rocket_contrib 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -841,6 +886,17 @@ dependencies = [ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serde_yaml" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "sha-1" version = "0.8.1" @@ -1136,6 +1192,14 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "yaml-rust" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "yansi" version = "0.4.0" @@ -1169,6 +1233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum devise_codegen 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "066ceb7928ca93a9bedc6d0e612a8a0424048b0ab1f75971b203d01420c055d7" "checksum devise_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cf41c59b22b5e3ec0ea55c7847e5f358d340f3a8d6d53a5cf4f1564967f96487" "checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +"checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" "checksum educe 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d27c760a73a13abb1ddf417c06bc2b8d14b0607dd19097316e0ca9412a971088" "checksum error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9" "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" @@ -1185,6 +1250,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum humansize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" "checksum hyper 0.10.16 (registry+https://github.com/rust-lang/crates.io-index)" = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +"checksum ifmt 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "abec215007c2ef1ccfb17a6bae6a87736fedb573860d2606ddec08b427666164" +"checksum ifmt-impl 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "744691ef283c5d8d4321f75cfa0e3b460b5adf3338bb7dcfd060f33e40300072" "checksum indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712d7b3ea5827fcb9d4fda14bf4da5f136f0db2ae9c8f4bd4e2d1c6fde4e6db2" "checksum inotify 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40b54539f3910d6f84fbf9a643efd6e3aa6e4f001426c0329576128255994718" "checksum inotify-sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0" @@ -1196,6 +1263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" "checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +"checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" "checksum lock_api 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e57b3997725d2b60dbec1297f6c2e2957cc383db1cebd6be812163f969c7d586" "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" @@ -1224,6 +1292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum pest_derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" "checksum pest_generator 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7b9fcf299b5712d06ee128a556c94709aaa04512c4dffb8ead07c5c998447fc0" "checksum pest_meta 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "df43fd99896fd72c485fe47542c7b500e4ac1e8700bf995544d1317a60ded547" +"checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27" "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" @@ -1245,6 +1314,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" "checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" "checksum serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)" = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7" +"checksum serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)" = "691b17f19fc1ec9d94ec0b5864859290dff279dbd7b03f017afda54eb36c3c35" "checksum sha-1 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "23962131a91661d643c98940b20fcaffe62d776a823247be80a48fcb8b6fce68" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum slug 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" @@ -1285,5 +1355,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +"checksum yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" "checksum yansi 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d60c3b48c9cdec42fb06b3b84b5b087405e1fa1c644a1af3930e4dfafe93de48" "checksum yansi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" diff --git a/Cargo.toml b/Cargo.toml index 4f04300..c1822ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,12 @@ rocket-download-response = "0.4.9" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_yaml = "0.8.11" json_dotpath = "0.1.2" +ifmt = "0.2.0" parking_lot = "0.10.0" +lazy_static = "1.4.0" [dependencies.rocket] version = "0.4.2" diff --git a/data/repository.yaml b/data/repository.yaml new file mode 100644 index 0000000..417e60a --- /dev/null +++ b/data/repository.yaml @@ -0,0 +1,32 @@ +model: + fields: + category: + type: "free_enum" + generic_code: + type: "free_enum" + code: + type: "string" + value: + type: "string" + package: + type: "free_enum" + mounting: + type: "enum" + options: + - "SMD" + - "Through-hole" + - "Screw" + quantity: + type: "int" + min: 0 + location: + type: "free_enum" + sublocation: + label: "Sub-location" + type: "string" + tags: + type: "free_tags" + note: + type: "text" + checkbox: + type: "bool" diff --git a/src/main.rs b/src/main.rs index 76f3a8b..cb86254 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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>, +} #[get("/")] fn index(store : State>) -> 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="")] -fn add_part(store : State>, record : Form) -> Redirect { - store.write().add(record.into_inner()); - Redirect::to(uri!(index)) -} +//#[post("/add", data="")] +//fn add_part(store : State>, record : Form) -> 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(); } diff --git a/src/store/form.rs b/src/store/form.rs new file mode 100644 index 0000000..e71a5d3 --- /dev/null +++ b/src/store/form.rs @@ -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 = 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>, + 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 + } +} diff --git a/src/store/mod.rs b/src/store/mod.rs index cb217a5..44344ca 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -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 + path : PathBuf, + pub model: Model, + pub items : HashMap, + 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>, + pub free_tags : HashMap>, } -fn load_file_or(file : impl AsRef, def : String) -> String { - let mut file = match File::open(file) { - Ok(file) => file, - Err(_) => return def - }; - - let mut buf = String::new(); - if file.read_to_string(&mut buf).is_err() { - return def; - } - - buf +/// 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() -> Self { - let mut file = load_file_or("inventory.json", "[]".to_string()); + pub fn new(path: impl AsRef) -> 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 { - parts: serde_json::from_str(&file).unwrap(), + 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 add(&mut self, part : Part) { - self.parts.push(part); + 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."); - self.persist() + let serialized = serde_json::to_string(&self.items).expect("Error serialize."); + file.write(serialized.as_bytes()).expect("Error write data file"); } +} - pub fn persist(&self) { - let mut file = OpenOptions::new().write(true).create(true).truncate(true).open("inventory.json").unwrap(); +fn load_file(path: impl AsRef) -> 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, def : impl Into) -> String { + let mut file = match File::open(file) { + Ok(file) => file, + Err(_) => return def.into() + }; - file.write(serde_json::to_string(&self.parts).unwrap().as_bytes()).unwrap(); + let mut buf = String::new(); + if file.read_to_string(&mut buf).is_err() { + return def.into(); } + + buf } diff --git a/src/store/model.rs b/src/store/model.rs new file mode 100644 index 0000000..8d325aa --- /dev/null +++ b/src/store/model.rs @@ -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, +} + +/// 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, + /// Highest allowed value + #[serde(default)] + max: Option, + /// Default value + #[serde(default)] + default: i64, + }, + + /// Floating point entry + Float { + /// Lowest allowed value + #[serde(default)] + min: Option, + /// Highest allowed value + #[serde(default)] + max: Option, + /// 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, + /// Default option (if not the first) + #[serde(default)] + default: Option, + }, + + /// 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, + }, + + /// Tags with a fixed set of options to choose from + Tags { + /// Options to choose from + options: Vec, + }, + + /// 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, + }, +} diff --git a/templates/form_macros.html.tera b/templates/form_macros.html.tera new file mode 100644 index 0000000..b194a32 --- /dev/null +++ b/templates/form_macros.html.tera @@ -0,0 +1,67 @@ +{% macro text(field) %} + + +{% endmacro %} + +{% macro longtext(field) %} + + +{% endmacro %} + +{% macro free_select(field) %} + + + + +{% for option in field.options %} + +{% endmacro %} + +{% macro number(field) %} + + +{% endmacro %} + +{% macro checkbox(field) %} + + +{% endmacro %} + +{% macro select(field) %} + + +{% endmacro %} diff --git a/templates/index.html.tera b/templates/index.html.tera index 5e023b2..d8e0888 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -1,16 +1,47 @@ - +{% extends "layout" %} +{% import "form_macros" as form %} + +{% block title -%} + Form +{%- endblock title %} + +{% block content %}
- Name:
- Qty:
- Location:
+ {% for field in fields %} +
+ {% if field.kind == "string" %} + + {{ form::text(field=field) }} + + {% elif field.kind == "text" %} + + {{ form::longtext(field=field) }} + + {% elif field.kind == "number" %} + + {{ form::number(field=field) }} + + {% elif field.kind == "bool" %} + + {{ form::checkbox(field=field) }} + + {% elif field.kind == "select" %} + + {{ form::select(field=field) }} + + {% elif field.kind == "free_select" %} + + {{ form::free_select(field=field) }} + + {% else %} + + {{ field.key }} + + {% endif %} +
+ {% endfor %} +
- -
    -{% for record in records %} -
  • - {{ record.name }} x {{ record.quantity }} in {{ record.location }} -
  • -{% endfor %} -
+{% endblock content %} diff --git a/templates/layout.html.tera b/templates/layout.html.tera new file mode 100644 index 0000000..7bbe1e2 --- /dev/null +++ b/templates/layout.html.tera @@ -0,0 +1,11 @@ + + + + + {% block title %}{% endblock title %} + + + +{% block content %}{% endblock content %} + + diff --git a/templates/static/style.css b/templates/static/style.css new file mode 100644 index 0000000..4baf9dc --- /dev/null +++ b/templates/static/style.css @@ -0,0 +1,27 @@ +*, *::before, *::after { + box-sizing: border-box; +} + +form .Row { + display: flex; + padding: .25rem; +} + +form .Row label { + flex-shrink: 0; + width: 10rem; + text-align: right; + display: inline-block; + padding-right: .5rem; +} + +form .Row input, +form .Row select { + height: 2.1rem; +} + +form .Row textarea { + flex-shrink: 1; + width: 30rem; + height: 6rem; +}