diff options
-rw-r--r-- | artifacts/layout.html | 28 | ||||
-rw-r--r-- | artifacts/ui-demo.html | 210 | ||||
-rw-r--r-- | src/config.rs | 54 | ||||
-rw-r--r-- | src/error.rs | 23 | ||||
-rw-r--r-- | src/main.rs | 76 | ||||
-rw-r--r-- | src/repo/mod.rs | 25 |
6 files changed, 83 insertions, 333 deletions
diff --git a/artifacts/layout.html b/artifacts/layout.html deleted file mode 100644 index a06d6d0..0000000 --- a/artifacts/layout.html +++ /dev/null @@ -1,28 +0,0 @@ -<!DOCTYPE html> -<html> - <body> - <ul> - <li>/: root, filter, search</li> - <li>/parts: raw parts table</li> - <li>/parts.csv: CSV download of parts table</li> - <li>/cars: raw cars table</li> - <li>/cars.csv: CSV download of cars table</li> - <li>/suggestions: suggestion input form and appoval list</li> - <li> - /login: all the logins - <ul> - <li>/login/facebook</li> - <li>/login/google</li> - <li>/login/microsoft</li> - <li>/login/gitea</li> - </ul> - </li> - <li>/api/v1: JSON API</li> - <ul> - <li>/api/v1/parts</li> - <li>/api/v1/cars</li> - <li>/api/v1/suggestions</li> - </ul> - </ul> - </body> -</html> diff --git a/artifacts/ui-demo.html b/artifacts/ui-demo.html deleted file mode 100644 index 09dc9cb..0000000 --- a/artifacts/ui-demo.html +++ /dev/null @@ -1,210 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <style> - :root { - --balboa: #195970; - } - - body { - margin: 0; - padding: 0; - } - - .model::before { - content: "➥"; - } - - .topbar { - background-color: var(--balboa); - box-shadow: 0 0 0.5em black; - position: relative; - height: 3em; - z-index: 1; - } - - .topbar ul { - margin: 0; - padding: 0; - overflow: hidden; - } - - .topbar li { - float: left; - display: block; - padding: 14px 14px; - } - - .topbar li.right { - float: right; - } - - .topbar li a { - color: white; - text-decoration: none; - } - - .sidebar { - background-color: var(--balboa); - box-shadow: inset -0.5em 0 0.5em -0.5em black; - z-index: -1; - position: fixed; - top: 3em; - left: 0; - width: 20%; - height: 100%; - transition: 0.25s; - } - - .sidebar button { - display: block; - background: none; - border: 0 none; - cursor: pointer; - color: white; - } - - .model { - margin-left: 20px; - } - - .model::before { - content: "➥"; - } - - #hidden:target .sidebar { - left: -20%; - } - - .menuToggle a.openMenu { - display: none; - } - - #hidden:target .menuToggle a.closeMenu { - display: none; - } - - #hidden:target .menuToggle a.openMenu { - display: block; - } - - #hidden:target section { - margin-left: 0; - } - - .catalogTable { - width: 100%; - border-collapse: collapse; - } - .catalogTable td, - .catalogTable th { - padding: 0.5em; - } - - .catalogTable tr:nth-child(even) { - background-color: #f2f2f2; - } - .catalogTable tr:hover { - background-color: #ddd; - } - .catalogTable th { - padding-top: 12px; - padding-bottom: 12px; - text-align: left; - background-color: var(--balboa); - color: white; - } - - input[type="search"] { - text-align: center; - width: 100%; - } - - section { - margin-left: 20%; - transition: margin-left 0.25s; - padding-top: 2em; - padding-left: 2em; - padding-right: 2em; - } - </style> - </head> - <body id="hidden"> - <nav class="topbar"> - <ul> - <li> - <div class="menuToggle"> - <a class="openMenu" href="#" - ><span - ><svg style="width: 24px; height: 24px" viewBox="0 0 24 24"> - <path - fill="currentColor" - d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" - /></svg></span - ></a> - <a class="closeMenu" href="#hidden" - ><span - ><svg style="width: 24px; height: 24px" viewBox="0 0 24 24"> - <path - fill="currentColor" - d="M21,15.61L19.59,17L14.58,12L19.59,7L21,8.39L17.44,12L21,15.61M3,6H16V8H3V6M3,13V11H13V13H3M3,18V16H16V18H3Z" - /></svg></span - ></a> - </div> - </li> - <li><a href="#">Index</a></li> - <li><a href="#">Parts</a></li> - <li><a href="#">Cars</a></li> - <li><a href="#">Suggestions</a></li> - <li><a href="#">About</a></li> - <li class="right"><a href="#">Login</a></li> - </ul> - </nav> - - <nav class="sidebar"> - {{#each makes}} - <form action="/parts"> - {{#if (eq this ../selected_make)}} - <button disabled class="make"> - {{ this }} - </button> - <input type="hidden" name="make" value="{{ ../selected_make }}" /> - {{#each ../models}} - {{#if (eq this ../../selected_model)}} - <button disabled class="model">{{ this }}</button> - {{else}} - <button class="model" name="model" value="{{ this }}"> - {{ this }} - </button> - {{/if}} - {{else}} - <button disabled class="model">(No models found)</button> - {{/each}} - {{else}} - <button class="make" name="make" value="{{ this }}">{{ this }}</button> - {{/if}} - </form> - {{else}} - No makes found. - {{/each}} - </nav> - - <section> - <form> - <input autofocus type="search" placeholder="hit enter to search" /> - </form> - </section> - - <section> - <table class="catalogTable"> - <tbody> - {{#each parts}} - <tr> - <td>{{ this }}</td> - </tr> - {{/each}} - </tbody> - </table> - </section> - </body> -</html> diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index da65385..0000000 --- a/src/config.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::TwinHError; -use once_cell::sync::Lazy; -use std::{env, error::Error, net::IpAddr, net::Ipv4Addr, net::SocketAddr}; - -pub static CONFIG_INSTANCE: Lazy<Config> = Lazy::new(|| match Config::new() { - Ok(c) => c, - Err(e) => panic!("twinh: config error: {}", e), -}); - -#[derive(Clone, Debug)] -pub struct Config { - pub bind_addr: SocketAddr, - pub db_path: String, -} - -impl Default for Config { - fn default() -> Self { - Self { - bind_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 5353), - db_path: String::from("/var/db/twinh"), - } - } -} - -impl Config { - fn new() -> Result<Self, Box<dyn Error>> { - let mut args = env::args().skip(1); - - let db_path = args - .next() - .ok_or_else(|| TwinHError(String::from("database directory not provided")))?; - - let mut config = Config { - bind_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 5353), - db_path, - }; - - while let Some(arg) = args.next() { - match arg.as_str() { - "--addr" => { - let addr = args.next().unwrap_or_default().parse()?; - config.bind_addr.set_ip(addr); - } - "--port" => { - let port = args.next().unwrap_or_default().parse()?; - config.bind_addr.set_port(port); - } - _ => {} - }; - } - - Ok(config) - } -} diff --git a/src/error.rs b/src/error.rs index 943c3ec..ef0ef1d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,9 @@ use bincode::Error as bincode_e; +use hyper::Error as hyper_e; use sled::Error as sled_e; use std::fmt; +use std::net::AddrParseError; +use std::num::ParseIntError; #[derive(Debug)] pub struct TwinHError(pub String); @@ -9,7 +12,7 @@ impl std::error::Error for TwinHError {} impl std::fmt::Display for TwinHError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "Twin H-Power: {}", self.0) + write!(f, "twinh: {}", self.0) } } @@ -24,3 +27,21 @@ impl From<bincode_e> for TwinHError { Self(format!("(de)serialization error: {}", e)) } } + +impl From<AddrParseError> for TwinHError { + fn from(e: AddrParseError) -> Self { + Self(format!("failed to parse addr: {}", e)) + } +} + +impl From<ParseIntError> for TwinHError { + fn from(e: ParseIntError) -> Self { + Self(format!("failed to parse port: {}", e)) + } +} + +impl From<hyper_e> for TwinHError { + fn from(e: hyper_e) -> Self { + Self(format!("server error: {}", e)) + } +} diff --git a/src/main.rs b/src/main.rs index 02fdc2e..0c7766a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,10 @@ -use crate::{config::CONFIG_INSTANCE, error::TwinHError}; +use crate::error::TwinHError; use hyper::{ service::{make_service_fn, service_fn}, Server, }; -use std::env; +use std::{env, net::IpAddr, net::Ipv4Addr, net::SocketAddr}; -mod config; mod error; mod import; mod models; @@ -14,9 +13,19 @@ mod routes; mod templates; #[tokio::main] -async fn main() -> Result<(), Box<dyn std::error::Error>> { - // handle non-config args - for arg in env::args().skip(1) { +async fn main() -> Result<(), TwinHError> { + // print help if there are no arguments + let mut args = env::args().skip(1).peekable(); + if args.peek().is_none() { + print_help(); + return Ok(()); + } + + // set default bind addr + let mut bind_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 5353); + + // handle arguments and options + while let Some(arg) = args.next() { match arg.as_str() { "--create-db" => { // create a fresh database and quit @@ -25,34 +34,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { } "--import" => { // import CSV data into database + let _source = args + .next() + .ok_or_else(|| TwinHError(String::from("import source not provided")))?; todo!(); } - "--help" | "-h" => { - // print help - print!( - "twinh: a home-grown classic car parts catalog\n\ - \nUsage: twinh <dir> [options]\n\ - <dir> your database directory (e.g. /var/db/twinh)\n\ - \nOptions:\n\ - --help | -h prints this message and exits\n\ - --addr an ip address to bind to (e.g. 127.0.0.1)\n\ - --port a port to bind to (e.g. 5353)\n\ - --create-db creates a fresh empty database; <dir> cannot exist yet\n\ - --import-cars imports CSV car data into the database\n\ - --import-parts imports CSV parts data into the database\n\ - " - ); - return Ok(()); + "--addr" => { + bind_addr.set_ip(args.next().unwrap_or_default().parse()?); + } + "--port" => { + bind_addr.set_port(args.next().unwrap_or_default().parse()?); } - unknown => { - panic!("unknown option: {}", unknown); + _ => { + // if not the last argument (the database) then it's unknown + if args.peek().is_some() { + print_help(); + return Ok(()); + } } }; } - // gather config - let bind_addr = CONFIG_INSTANCE.bind_addr; - // create primary listener let make_svc = make_service_fn(move |_conn| async { Ok::<_, TwinHError>(service_fn(routes::router)) }); @@ -62,10 +64,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { let graceful = server.with_graceful_shutdown(shutdown_signal()); // start and run until signal - if let Err(e) = graceful.await { - eprintln!("server error: {}", e); - } - + graceful.await?; Ok(()) } @@ -75,3 +74,18 @@ async fn shutdown_signal() { .await .expect("failed to install CTRL+C signal handler"); } + +fn print_help() { + print!( + "twinh: a home-grown classic car parts catalog\n\ + \nUsage: twinh [options] <dir>\n\ + <dir> your database directory (e.g. /var/db/twinh)\n\ + \nOptions:\n\ + --addr an ip address to bind to (e.g. 127.0.0.1)\n\ + --port a port to bind to (e.g. 5353)\n\ + --create-db creates a fresh empty database; <dir> cannot exist yet\n\ + --import-cars imports CSV car data into the database\n\ + --import-parts imports CSV parts data into the database\n\ + " + ); +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 45e9ce3..c7992c9 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,4 +1,3 @@ -use crate::config::CONFIG_INSTANCE; use crate::error::TwinHError; use crate::models::Car; use crate::models::Part; @@ -6,20 +5,28 @@ use bincode::deserialize; use constants::*; use once_cell::sync::Lazy; use sled::{Config, Db}; +use std::env; mod constants; -static REPO_INSTANCE: Lazy<Db> = Lazy::new(|| { - Config::default() - .path(&CONFIG_INSTANCE.db_path) - .temporary(true) - .open() - .expect("Couldn't open DB!") +pub static DB_PATH_INSTANCE: Lazy<String> = Lazy::new(|| { + env::args() + .skip(1) + .last() + .expect("database directory not provided") }); +static REPO_INSTANCE: Lazy<Db> = + Lazy::new( + || match Config::default().path(DB_PATH_INSTANCE.as_str()).open() { + Err(e) => panic!("failed to open database: {}", e), + Ok(db) => db, + }, + ); + pub fn create_demo_db() -> Result<(), TwinHError> { let db = sled::Config::default() - .path(&CONFIG_INSTANCE.db_path) + .path(DB_PATH_INSTANCE.as_str()) .create_new(true) .open()?; let cars_tree = db.open_tree(CARS_TREE)?; @@ -28,7 +35,7 @@ pub fn create_demo_db() -> Result<(), TwinHError> { pub fn create_new_db() -> Result<(), TwinHError> { sled::Config::default() - .path(&CONFIG_INSTANCE.db_path) + .path(DB_PATH_INSTANCE.as_str()) .create_new(true) .open()?; Ok(()) |