From 148574ed6e7cc526762e8ed51643c4e33a7a5c52 Mon Sep 17 00:00:00 2001 From: Nilstrieb Date: Fri, 24 Sep 2021 22:52:20 +0200 Subject: [PATCH] completed schema and already added some docs --- examples/compiler.rs | 1 - src/lib.rs | 107 +++++++++++++++--------------- src/macros.rs | 2 +- src/schema.rs | 150 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 187 insertions(+), 73 deletions(-) diff --git a/examples/compiler.rs b/examples/compiler.rs index e4cd1f7..dfd51eb 100644 --- a/examples/compiler.rs +++ b/examples/compiler.rs @@ -1,5 +1,4 @@ use badargs::arg; -use badargs::CliArg; arg!(OutFile: "output", "o" -> Option); arg!(Force: "force", "f" -> bool); diff --git a/src/lib.rs b/src/lib.rs index 081cbf5..8d55aa8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,13 +2,61 @@ mod macros; mod schema; use crate::parse::CliArgs; -use crate::schema::{IntoSchema, SchemaKind}; +use crate::schema::{IntoSchema, Schema, SchemaKind}; pub use error::ArgError; pub use macros::*; pub type Result = std::result::Result; +/// +/// Parses the command line arguments based on the provided schema S +pub fn badargs() -> Result +where + S: IntoSchema, +{ + let arg_schema = Schema::create::()?; + + let args = CliArgs::from_args(arg_schema, std::env::args_os())?; + + Ok(BadArgs { args }) +} + +/// +/// Implemented by a user provided type that contains all info for a single command line argument +/// +/// This is mostly done using unit structs and the `arg!` macro +/// +/// ``` +/// # use badargs::arg; +/// arg!(OutFile: "output", "o" -> Option); +/// // OutFile now implements CliArg +/// ``` +pub trait CliArg { + type Content: CliReturnValue; + + fn long() -> &'static str; + fn short() -> Option<&'static str>; +} + +/// The struct containing parsed argument information +#[derive(Debug, Clone, Default)] +pub struct BadArgs { + args: CliArgs, +} + +impl BadArgs { + /// Get the content of an argument by providing the type of the argument + pub fn get(&self) -> &T::Content + where + T: CliArg, + { + todo!() + } +} + +/// +/// A type that could be parsed from command line arguments pub trait CliReturnValue: sealed::SealedCliReturnValue { fn kind() -> schema::SchemaKind; } @@ -39,39 +87,9 @@ mod sealed { impl_!(String, Option, bool, usize, isize); } -pub trait CliArg { - type Content: CliReturnValue; - fn long() -> &'static str; - fn short() -> Option<&'static str>; -} - -#[derive(Debug, Clone, Default)] -pub struct BadArgs { - args: CliArgs, -} - -impl BadArgs { - pub fn get(&self) -> &T::Content - where - T: CliArg, - { - todo!() - } -} - -pub fn badargs() -> Result -where - S: IntoSchema, -{ - let arg_schema = schema::parse_schema::()?; - - let args = CliArgs::from_args(arg_schema, std::env::args_os())?; - - Ok(BadArgs { args }) -} - mod error { - #[derive(Debug, Clone)] + /// The error type for `badargs` + #[derive(Debug, Clone, Eq, PartialEq)] pub enum ArgError { InvalidUtf8, NameAlreadyExists(&'static str), @@ -97,7 +115,7 @@ mod parse { impl CliArgs { pub fn from_args(_schema: Schema, args: impl Iterator) -> Result { - let mut result = Self::default(); + let result = Self::default(); let mut args = args; while let Some(_arg) = args.next() {} @@ -105,24 +123,3 @@ mod parse { } } } - -#[cfg(test)] -mod test { - use crate::CliArg; - - struct OutFile; - impl CliArg for OutFile { - type Content = Option; - - fn long() -> &'static str { - "output" - } - - fn short() -> Option<&'static str> { - Some("o") - } - } - - #[test] - fn get_single_schema() {} -} diff --git a/src/macros.rs b/src/macros.rs index d1d9621..cf3e4ca 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -10,7 +10,7 @@ macro_rules! arg { #[derive(Default)] struct $name; - impl ::badargs::CliArg for $name { + impl $crate::CliArg for $name { type Content = $result; fn long() -> &'static str { diff --git a/src/schema.rs b/src/schema.rs index 1e4a191..cd6a3fa 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,7 +1,16 @@ +//! +//! Generates CLI argument schemas based on generic types +//! +//! This makes the interface of this crate fully type-safe! (and kind of cursed) + use super::Result; use crate::{ArgError, CliArg, CliReturnValue}; use std::collections::HashMap; +/// +/// The type of value the argument returns +/// +/// This could *maybe* also be solved with trait objects but lets keep this for now #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum SchemaKind { String, @@ -11,27 +20,60 @@ pub enum SchemaKind { UNum, } -#[derive(Debug, Clone, Eq, PartialEq)] +/// +/// A single command in the schema +#[derive(Debug, Clone, Eq, PartialEq, Copy)] pub struct SchemaCommand { - short: Option<&'static str>, kind: SchemaKind, } +/// +/// A runtime representation of the schema type #[derive(Debug, Clone, Default, Eq, PartialEq)] pub struct Schema { - commands: HashMap<&'static str, SchemaCommand>, + longs: HashMap<&'static str, SchemaCommand>, + shorts: HashMap<&'static str, SchemaCommand>, } impl Schema { - pub fn add_command(&mut self, name: &'static str, command: SchemaCommand) -> Result<()> { - if let Some(_) = self.commands.insert(name, command) { - Err(ArgError::NameAlreadyExists(name)) + /// Creates the `Schema` from the generic parameter `S` + pub fn create() -> Result + where + S: IntoSchema, + { + let mut schema = Schema::default(); + S::add_schema(&mut schema)?; + Ok(schema) + } + + fn add_command(&mut self, long_name: &'static str, command: SchemaCommand) -> Result<()> { + if let Some(_) = self.longs.insert(long_name, command) { + Err(ArgError::NameAlreadyExists(long_name)) + } else { + Ok(()) + } + } + + fn add_short_command( + &mut self, + short_name: &'static str, + command: SchemaCommand, + ) -> Result<()> { + if let Some(_) = self.shorts.insert(short_name, command) { + Err(ArgError::NameAlreadyExists(short_name)) } else { Ok(()) } } } +/// +/// This trait allows a type to be added to the schema +/// +/// Any type that implements `CliArg` automatically gets this trait implementation for free +/// +/// This has to be a separate trait because it's also implemented by the tuple, allowing for +/// multiple arguments pub trait IntoSchema { fn add_schema(schema: &mut Schema) -> Result<()>; } @@ -48,15 +90,6 @@ where } } -pub fn parse_schema() -> Result -where - S: IntoSchema, -{ - let mut schema = Schema::default(); - S::add_schema(&mut schema)?; - Ok(schema) -} - /// Create the Schema from the CliArg type impl IntoSchema for T where @@ -66,6 +99,91 @@ where let kind = T::Content::kind(); let name = T::long(); let short = T::short(); - schema.add_command(name, SchemaCommand { short, kind }) + let command = SchemaCommand { kind }; + if let Some(short_name) = short { + schema.add_short_command(short_name, command)?; + } + schema.add_command(name, command) + } +} + +#[cfg(test)] +mod test { + use crate::schema::{Schema, SchemaCommand, SchemaKind}; + use crate::{arg, ArgError}; + + arg!(OutFile: "output", "o" -> Option); + arg!(Force: "force", "f" -> bool); + arg!(SetUpstream: "set-upstream" -> String); + arg!(OutFile2: "output", "o" -> Option); + + #[test] + fn one_command_schema() { + let schema = Schema::create::().unwrap(); + let out_file = SchemaCommand { + kind: SchemaKind::OptionString, + }; + assert_eq!(schema.longs.get("output"), Some(&out_file)); + assert_eq!(schema.shorts.get("o"), Some(&out_file)); + assert_eq!(schema.longs.get("o"), None); + assert_eq!(schema.shorts.get("output"), None); + } + + #[test] + fn two_command_schema() { + let schema = Schema::create::<(OutFile, Force)>().unwrap(); + let out_file = SchemaCommand { + kind: SchemaKind::OptionString, + }; + let force = SchemaCommand { + kind: SchemaKind::Bool, + }; + + assert_eq!(schema.longs.get("output"), Some(&out_file)); + assert_eq!(schema.shorts.get("o"), Some(&out_file)); + assert_eq!(schema.longs.get("o"), None); + assert_eq!(schema.shorts.get("output"), None); + + assert_eq!(schema.longs.get("force"), Some(&force)); + assert_eq!(schema.shorts.get("f"), Some(&force)); + assert_eq!(schema.longs.get("f"), None); + assert_eq!(schema.shorts.get("force"), None); + } + + #[test] + fn three_command_schema() { + let schema = Schema::create::<(OutFile, (Force, SetUpstream))>().unwrap(); + let out_file = SchemaCommand { + kind: SchemaKind::OptionString, + }; + let force = SchemaCommand { + kind: SchemaKind::Bool, + }; + let set_upstream = SchemaCommand { + kind: SchemaKind::String, + }; + + assert_eq!(schema.longs.get("output"), Some(&out_file)); + assert_eq!(schema.shorts.get("o"), Some(&out_file)); + assert_eq!(schema.longs.get("o"), None); + assert_eq!(schema.shorts.get("output"), None); + + assert_eq!(schema.longs.get("force"), Some(&force)); + assert_eq!(schema.shorts.get("f"), Some(&force)); + assert_eq!(schema.longs.get("f"), None); + assert_eq!(schema.shorts.get("force"), None); + + assert_eq!(schema.longs.get("set-upstream"), Some(&set_upstream)); + assert_eq!(schema.shorts.get("set-upstream"), None); + } + + #[test] + fn double_error() { + let schema = Schema::create::<(OutFile, OutFile2)>(); + assert!(matches!( + schema, + // it doesn't matter which one gets reported first + Err(ArgError::NameAlreadyExists("output")) | Err(ArgError::NameAlreadyExists("o")) + )); } }