diff options
author | Ben Bridle <ben@derelict.engineering> | 2025-03-03 20:51:01 +1300 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2025-03-03 20:54:45 +1300 |
commit | 8a43a02b6950455aedbbdbee737bee1654aa91ef (patch) | |
tree | 64e31ff1cfbbdce22e104adcb1ad81f051019ca1 | |
parent | ea70fa89659e5cf1a9d4ca6ea31fb67f7a2cc633 (diff) | |
download | switchboard-8a43a02b6950455aedbbdbee737bee1654aa91ef.zip |
Implement error reporting
The library has been redesigned so that all queries can be entered
before any values are parsed. This allows all errors unrelated to value
parsing to be displayed as a batch.
-rw-r--r-- | src/lib.rs | 112 | ||||
-rw-r--r-- | src/main.rs | 60 | ||||
-rw-r--r-- | src/query.rs | 236 | ||||
-rw-r--r-- | src/switchboard.rs | 145 | ||||
-rw-r--r-- | src/value.rs | 112 |
5 files changed, 454 insertions, 211 deletions
@@ -1,5 +1,12 @@ mod query; +mod switchboard; +mod value; + pub use query::*; +pub use switchboard::*; +pub use value::*; + +use log::*; pub enum SwitchName { @@ -7,107 +14,24 @@ pub enum SwitchName { Long(String), } - -pub struct Switchboard { - // First positional argument. - pub program: String, - // Positional arguments, stored in reverse order. - pub positional: Vec<String>, - // Named arguments, stored in forward order. - pub switches: Vec<(SwitchName, Option<String>)>, - // All arguments following a '--' token, stored in forward order. - pub unprocessed: Option<Vec<String>>, -} - -impl Switchboard { - pub fn parse(mut args: Vec<String>) -> Self { - let mut positional = Vec::new(); - let mut switches = Vec::new(); - let mut unprocessed = None; - args.reverse(); - - while let Some(arg) = args.pop() { - if arg.is_empty() { - continue; - } else if arg == "--" { - args.reverse(); - unprocessed = Some(args); - break; - } else if let Some(arg) = arg.strip_prefix("--") { - let (name, value) = match arg.split_once("=") { - Some((name, "")) => (name, None), - Some((name, value)) => (name, Some(value.to_string())), - None => (arg, None), - }; - if name.is_empty() { continue } - switches.push((SwitchName::Long(name.to_string()), value)); - } else if let Some(arg) = arg.strip_prefix("-") { - let (name, value) = match arg.split_once("=") { - Some((name, "")) => (name, None), - Some((name, value)) => (name, Some(value.to_string())), - None => (arg, None), - }; - let chars: Vec<char> = name.chars().collect(); - if chars.len() != 1 { continue } - switches.push((SwitchName::Short(chars[0]), value)); - } else { - positional.push(arg) - } - } - // Reverse order of positional arguments and move program name. - positional.reverse(); - let program = positional.pop().unwrap(); - Self { - program, positional, switches, unprocessed, - } - } - - pub fn from_env() -> Self { - let mut args = Vec::new(); - for arg_os in std::env::args_os() { - args.push(arg_os.to_string_lossy().to_string()); - } - Self::parse(args) - } - - pub fn named(&mut self, name: &str) -> NamedSwitchQuery { - validate_name(name); - NamedSwitchQuery { - switchboard: self, - name: name.to_string(), - short: None, - default: None, - quick: None, - } - } - - pub fn positional(&mut self, name: &str) -> PositionalSwitchQuery { - validate_name(name); - PositionalSwitchQuery { - switchboard: self, - name: name.to_string(), - default: None, +impl std::fmt::Display for SwitchName { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + SwitchName::Short(c) => write!(f, "-{c}"), + SwitchName::Long(s) => write!(f, "--{s}"), } } - - /// Check the next positional argument without consuming it. - pub fn peek(&self) -> Option<&str> { - self.positional.last().map(|p| p.as_str()) - } - - /// Consume the next positional argument. - pub fn pop(&mut self) { - self.positional.pop(); - } } - -pub enum SwitchboardError { +pub enum QueryError { + MissingNamed(String), + MissingPositional(String), + // String is the debug name of the switch + Repeated(String), } - fn validate_name(name: &str) { if !name.chars().all(|c| c.is_ascii_lowercase() || c.is_digit(10) || c == '-') { - panic!("Invalid name for argument: {name:?}"); + log::fatal!("Invalid name for argument: {name:?}"); } } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0f24c2c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,60 @@ +use switchboard::*; + + +fn main() { + let mut args = Switchboard::from_env(); + args.named("version"); + args.named("verbose").short('v'); + match args.peek() { + Some("run") => { args.pop(); run_program(args) } + Some("asm") => { args.pop(); assemble_program(args) } + _ => run_program(args), + } +} + +fn run_program(mut args: Switchboard) { + println!("RUN PROGRAM"); + + args.positional("source").required(); + args.named("debug").short('d'); + args.named("fullscreen").short('f'); + args.named("zoom").short('z').quick("3").default("1"); + args.raise_errors(); + + let verbose = args.get("verbose").as_bool(); + let version = args.get("version").as_bool(); + let debug = args.get("debug").as_bool(); + let fullscreen = args.get("fullscreen").as_bool(); + let zoom = args.get("zoom").as_u32(); + let source = args.get("source").as_path(); + + println!("Verbose: {verbose:?}"); + println!("Version: {version:?}"); + println!("Source path: {source:?}"); + println!("Debug: {debug:?}"); + println!("Fullscreen: {fullscreen:?}"); + println!("Zoom: {zoom:?}"); +} + +fn assemble_program(mut args: Switchboard) { + println!("ASSEMBLE PROGRAM"); + + args.positional("source"); + args.positional("destination"); + args.positional("extension").default("brc"); + args.named("no-libs"); + args.named("no-project-libs"); + args.raise_errors(); + + let source_path = args.get("source").as_path(); + let destination_path = args.get("destination").as_path(); + let extension = args.get("extension").as_string(); + let no_libs = args.get("no-libs").as_bool(); + let no_project_libs = args.get("no-project-libs").as_bool(); + + println!("Source path: {source_path:?}"); + println!("Destination path: {destination_path:?}"); + println!("Extension: {extension:?}"); + println!("No libs: {no_libs:?}"); + println!("No project libs: {no_project_libs:?}"); +} diff --git a/src/query.rs b/src/query.rs index 4109488..5a9d4ae 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,9 +1,5 @@ use crate::*; -use log::fatal; - -use std::path::PathBuf; - pub struct NamedSwitchQuery<'a> { /// The borrowed switchboard instance. @@ -12,31 +8,99 @@ pub struct NamedSwitchQuery<'a> { pub name: String, /// Short switch name. pub short: Option<char>, - /// The default value if the switch hasn't been provided. - pub default: Option<String>, /// The default value if the switch has been provided with no value. pub quick: Option<String>, + /// Documentation string. + pub doc: Option<String>, + /// True if this query has already been committed. + pub committed: bool, } impl NamedSwitchQuery<'_> { + pub fn doc(mut self, doc: &str) -> Self { + self.doc = Some(doc.to_string()); + self + } + pub fn short(mut self, short: char) -> Self { if !short.is_ascii_alphanumeric() { - panic!("Invalid short name for argument: {short:?}"); + fatal!("Invalid short name for argument: {short:?}"); } self.short = Some(short); self } - pub fn default(mut self, value: &str) -> Self { - self.default = Some(value.to_string()); - self - } - pub fn quick(mut self, value: &str) -> Self { self.quick = Some(value.to_string()); self } + pub fn optional(&mut self) { + let value = self.get_value(); + self.insert(value); + } + + pub fn default(&mut self, default: &str) { + match self.get_value() { + Some(value) => self.insert(Some(value)), + None => self.insert(Some(Some(default.to_string()))), + }; + } + + pub fn required(&mut self) { + match self.get_value() { + Some(value) => self.insert(Some(value)), + None => { + let error = QueryError::MissingNamed(self.debug_name()); + self.switchboard.errors.push(error); + } + } + } + + fn insert(&mut self, value: Option<Option<String>>) { + let queried = QueriedValue { + doc: self.doc.clone(), + name: self.name.clone(), + variant: QueryVariant::Named(self.debug_name()), + value, + }; + if let Some(_) = self.switchboard.values.insert(self.name.clone(), queried) { + error!("Duplicate query for name {:?}", self.name.clone()); + } + self.committed = true; + } + + fn get_value(&mut self) -> Option<Option<String>> { + // Find all matches first. + let mut matches = Vec::new(); + for (i, switch) in self.switchboard.switches.iter().enumerate() { + match &switch.0 { + SwitchName::Short(other) => if let Some(short) = self.short { + if short == *other { matches.push(i) } + } + SwitchName::Long(other) => if self.name == *other { + matches.push(i); + }, + }; + } + // Return a value. + match matches.len() { + 0 => None, + 1 => { + let found = self.switchboard.switches.remove(matches[0]).1; + match found { + Some(string) => Some(Some(string)), + None => Some(self.quick.clone()), + } + } + _ => { + let error = QueryError::Repeated(self.debug_name()); + self.switchboard.errors.push(error); + None + } + } + } + fn debug_name(&self) -> String { let mut debug_name = format!("--{}", self.name); if let Some(short) = self.short { @@ -46,135 +110,73 @@ impl NamedSwitchQuery<'_> { } } +impl Drop for NamedSwitchQuery<'_> { + fn drop(&mut self) { + if !self.committed { + self.optional(); + } + } +} + + pub struct PositionalSwitchQuery<'a> { /// The borrowed switchboard instance. pub switchboard: &'a mut Switchboard, /// The display name of this argument. pub name: String, - /// The default value if the switch hasn't been provided. - pub default: Option<String>, + /// Documentation string. + pub doc: Option<String>, + /// True if this query has already been committed. + pub committed: bool, } impl PositionalSwitchQuery<'_> { - pub fn default(mut self, value: &str) -> Self { - self.default = Some(value.to_string()); + pub fn doc(mut self, doc: &str) -> Self { + self.doc = Some(doc.to_string()); self } -} - - -macro_rules! as_number { - ($type:ty, $name:expr, $error:ty) => { - paste::paste! { - fn [< as_ $type >](&mut self) -> $type { - self.[< as_ $type _opt >]().unwrap_or_else(|| self.missing("number")) - } - fn [< as_ $type _opt>](&mut self) -> Option<$type> { - self.get_value().map(|v| v.trim().parse().unwrap_or_else( - |e: $error| self.error(&v, $name, e.to_string()))) - } - } - }; -} - -pub trait SwitchQuery { - fn get_name(&self) -> String; - - fn get_value(&mut self) -> Option<String>; - - fn error(&self, value: &str, ty: &str, err: String) -> ! { - let name = self.get_name(); - fatal!("The {name} with value {value:?} could not be parsed as {ty}: {err}"); + pub fn optional(&mut self) { + let value = self.switchboard.pop(); + self.insert(value); } - fn missing(&self, ty: &str) -> ! { - let name = self.get_name(); - fatal!("The required {name} that takes a {ty} value was not provided"); + pub fn default(&mut self, default: &str) { + match self.switchboard.pop() { + Some(value) => self.insert(Some(value)), + None => self.insert(Some(default.to_string())), + }; } - fn as_bool(&mut self) -> bool { - if let Some(value) = self.get_value() { - match value.to_lowercase().as_str() { - "y"|"yes"|"t"|"true" => true, - "n"|"no" |"f"|"false" => false, - _ => true, + pub fn required(&mut self) { + match self.switchboard.pop() { + Some(value) => self.insert(Some(value)), + None => { + let error = QueryError::MissingPositional(self.name.clone()); + self.switchboard.errors.push(error); } - } else { - false } } - fn as_path(&mut self) -> PathBuf { - self.as_path_opt().unwrap_or_else(|| self.missing("path")) - } - fn as_path_opt(&mut self) -> Option<PathBuf> { - if let Some(value) = self.get_value() { - if !value.is_empty() { - return Some(PathBuf::from(value)); - } + fn insert(&mut self, value: Option<String>) { + let queried = QueriedValue { + doc: self.doc.clone(), + name: self.name.clone(), + variant: QueryVariant::Positional(self.switchboard.i), + value: Some(value), + }; + if let Some(_) = self.switchboard.values.insert(self.name.clone(), queried) { + error!("Duplicate query for name {:?}", self.name.clone()); } - return None; + self.committed = true; } - - fn as_string(&mut self) -> String { - self.as_string_opt().unwrap_or_else(|| self.missing("string")) - } - fn as_string_opt(&mut self) -> Option<String> { - self.get_value() - } - - as_number!{ f32, "f32" , std::num::ParseFloatError } - as_number!{ f64, "f64" , std::num::ParseFloatError } - as_number!{ u8, "u8" , std::num::ParseIntError } - as_number!{ u16, "u16" , std::num::ParseIntError } - as_number!{ u32, "u32" , std::num::ParseIntError } - as_number!{ u64, "u64" , std::num::ParseIntError } - as_number!{ usize, "usize", std::num::ParseIntError } - as_number!{ i8, "i8" , std::num::ParseIntError } - as_number!{ i16, "i16" , std::num::ParseIntError } - as_number!{ i32, "i32" , std::num::ParseIntError } - as_number!{ i64, "i64" , std::num::ParseIntError } - as_number!{ isize, "isize", std::num::ParseIntError } } - -impl SwitchQuery for NamedSwitchQuery<'_> { - fn get_name(&self) -> String { - format!("argument {}", self.debug_name()) - } - - fn get_value(&mut self) -> Option<String> { - // Find all matches first. - let mut matches = Vec::new(); - for (i, switch) in self.switchboard.switches.iter().enumerate() { - match &switch.0 { - SwitchName::Short(other) => if let Some(short) = self.short { - if short == *other { matches.push(i) } } - SwitchName::Long(other) => if self.name == *other { - matches.push(i); }, - }; - } - match matches.len() { - 0 => self.default.clone(), - 1 => Some( - self.switchboard.switches.remove(matches[0]).1 - .or_else(|| self.quick.clone()) - .unwrap_or_else(String::new) - ), - _ => fatal!("The argument {} was passed more than once", self.debug_name()), +impl Drop for PositionalSwitchQuery<'_> { + fn drop(&mut self) { + if !self.committed { + self.optional(); } } } - - -impl SwitchQuery for PositionalSwitchQuery<'_> { - fn get_name(&self) -> String { - format!("positional argument {:?}", self.name) - } - - fn get_value(&mut self) -> Option<String> { - self.switchboard.positional.pop().or_else(|| self.default.clone()) - } -} diff --git a/src/switchboard.rs b/src/switchboard.rs new file mode 100644 index 0000000..f19db13 --- /dev/null +++ b/src/switchboard.rs @@ -0,0 +1,145 @@ +use crate::*; + +use std::collections::{HashMap, VecDeque}; + + +pub struct Switchboard { + // First positional argument. + pub program: String, + // Positional arguments, stored in reverse order. + pub positional: Vec<String>, + // Named arguments, stored in forward order. + pub switches: Vec<(SwitchName, Option<String>)>, + // All arguments following a '--' token, stored in forward order. + pub unprocessed: Option<Vec<String>>, + // All queried values. + pub values: HashMap<String, QueriedValue>, + // All query errors + pub errors: Vec<QueryError>, + // Total number of popped positional arguments + pub i: usize, +} + +impl Switchboard { + pub fn parse(args: Vec<String>) -> Self { + let mut positional = Vec::new(); + let mut switches = Vec::new(); + let mut unprocessed = None; + let mut args = VecDeque::from(args); + + while let Some(arg) = args.pop_front() { + if arg.is_empty() { + continue; + } else if arg == "--" { + unprocessed = Some(Vec::from(args)); + break; + } else if let Some(arg) = arg.strip_prefix("--") { + let (name, value) = match arg.split_once("=") { + Some((name, "")) => (name, None), + Some((name, value)) => (name, Some(value.to_string())), + None => (arg, None), + }; + if name.is_empty() { continue } + switches.push((SwitchName::Long(name.to_string()), value)); + } else if let Some(arg) = arg.strip_prefix("-") { + let (name, value) = match arg.split_once("=") { + Some((name, "")) => (name, None), + Some((name, value)) => (name, Some(value.to_string())), + None => (arg, None), + }; + let chars: Vec<char> = name.chars().collect(); + if chars.len() != 1 { continue } + switches.push((SwitchName::Short(chars[0]), value)); + } else { + positional.push(arg) + } + } + // Reverse order of positional arguments and move program name. + positional.reverse(); + let program = positional.pop().unwrap(); + let values = HashMap::new(); + let errors = Vec::new(); + let i = 0; + Self { + program, positional, switches, unprocessed, values, errors, i + } + } + + pub fn from_env() -> Self { + let mut args = Vec::new(); + for arg_os in std::env::args_os() { + args.push(arg_os.to_string_lossy().to_string()); + } + Self::parse(args) + } + + pub fn named(&mut self, name: &str) -> NamedSwitchQuery { + validate_name(name); + NamedSwitchQuery { + switchboard: self, + name: name.to_string(), + short: None, + quick: None, + doc: None, + committed: false, + } + } + + pub fn positional(&mut self, name: &str) -> PositionalSwitchQuery { + validate_name(name); + PositionalSwitchQuery { + switchboard: self, + name: name.to_string(), + doc: None, + committed: false, + } + } + + pub fn raise_errors(&self) { + if self.errors.is_empty() + && self.switches.is_empty() + && self.positional.is_empty(){ + return; + } + for (name, _value) in &self.switches { + error!("The switch {name} was not recognised") + } + if !self.positional.is_empty() { + error!("Too many positional arguments provided") + } + for error in &self.errors { + match error { + QueryError::MissingNamed(name) => { + error!("The {name} switch is required") + } + QueryError::MissingPositional(name) => { + error!("The <{name}> argument is required") + } + QueryError::Repeated(name) => { + error!("The {name} switch was passed multiple times") + } + } + } + std::process::exit(1); + } + + pub fn get(&mut self, name: &str) -> QueriedValue { + match self.values.remove(name) { + Some(value) => value, + None => panic!("Name has not been defined: {name:?}"), + } + } + + /// Check the next positional argument without consuming it. + pub fn peek(&self) -> Option<&str> { + self.positional.last().map(|p| p.as_str()) + } + + /// Consume the next positional argument. + pub fn pop(&mut self) -> Option<String> { + self.i += 1; + self.positional.pop() + } +} + + diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..99468fa --- /dev/null +++ b/src/value.rs @@ -0,0 +1,112 @@ +use crate::*; + +use std::path::PathBuf; + + +macro_rules! as_number { + ($type:ty, $name:expr, $error:ty) => { + paste::paste! { + pub fn [< as_ $type >](&mut self) -> $type { + self.[< as_ $type _opt >]().unwrap_or_else(|| self.missing("number")) + } + pub fn [< as_ $type _opt>](&mut self) -> Option<$type> { + self.value().as_ref().map(|v| v.trim().parse().unwrap_or_else( + |e: $error| self.error(&v, $name, e.to_string()))) + } + } + }; +} + + +pub struct QueriedValue { + pub doc: Option<String>, + pub name: String, + pub variant: QueryVariant, + /// Some if switch passed, then some if value passed. + pub value: Option<Option<String>>, +} + +pub enum QueryVariant { + Positional(usize), + Named(String), +} + +impl QueriedValue { + pub fn value(&self) -> Option<&String> { + match &self.value { + Some(value) => value.as_ref(), + None => None, + } + } + + pub fn as_bool(self) -> bool { + if let Some(value) = self.value { + if let Some(value) = value { + match value.to_lowercase().as_str() { + "y"|"yes"|"t"|"true" => true, + "n"|"no" |"f"|"false" => false, + _ => true, + } + } else { + true + } + } else { + false + } + } + + pub fn as_path(&mut self) -> PathBuf { + self.as_path_opt().unwrap_or_else(|| self.missing("path")) + } + pub fn as_path_opt(&mut self) -> Option<PathBuf> { + if let Some(value) = self.value() { + if !value.is_empty() { + return Some(PathBuf::from(value)); + } + } + return None; + } + + pub fn as_string(&mut self) -> String { + self.as_string_opt().unwrap_or_else(|| self.missing("string")) + } + pub fn as_string_opt(&mut self) -> Option<String> { + self.value().cloned() + } + + as_number!{ f32, "f32" , std::num::ParseFloatError } + as_number!{ f64, "f64" , std::num::ParseFloatError } + as_number!{ u8, "u8" , std::num::ParseIntError } + as_number!{ u16, "u16" , std::num::ParseIntError } + as_number!{ u32, "u32" , std::num::ParseIntError } + as_number!{ u64, "u64" , std::num::ParseIntError } + as_number!{ usize, "usize", std::num::ParseIntError } + as_number!{ i8, "i8" , std::num::ParseIntError } + as_number!{ i16, "i16" , std::num::ParseIntError } + as_number!{ i32, "i32" , std::num::ParseIntError } + as_number!{ i64, "i64" , std::num::ParseIntError } + as_number!{ isize, "isize", std::num::ParseIntError } + + + fn locator(&self) -> String { + match &self.variant { + QueryVariant::Positional(_) => { + let name = &self.name; + format!("<{name}> argument") + } + QueryVariant::Named(name) => { + format!("{name} switch") + } + } + } + + fn error(&self, value: &str, ty: &str, err: String) -> ! { + let locator = self.locator(); + fatal!("The value {value:?} passed to the {locator} could not be parsed as a {ty}: {err}"); + } + + fn missing(&self, _ty: &str) -> ! { + let locator = self.locator(); + fatal!("The {locator} is required"); + } +} |