diff --git a/option_parser/src/lib.rs b/option_parser/src/lib.rs index 67c12bd05..85d265f6a 100644 --- a/option_parser/src/lib.rs +++ b/option_parser/src/lib.rs @@ -3,6 +3,29 @@ // SPDX-License-Identifier: Apache-2.0 // +//! A parser for comma-separated `key=value` option strings. +//! +//! This crate provides [`OptionParser`], which parses strings of the form +//! `"key1=value1,key2=value2,..."` into a set of named options that can then +//! be retrieved and converted to various types. +//! +//! Values may be quoted with `"` to embed commas and other special characters, +//! and brackets `[` `]` are tracked so that list-valued options like +//! `topology=[1,2,3]` are not split at inner commas. +//! +//! # Example +//! +//! ``` +//! use option_parser::OptionParser; +//! +//! let mut parser = OptionParser::new(); +//! parser.add("size").add("mergeable"); +//! parser.parse("size=128M,mergeable=on").unwrap(); +//! +//! assert_eq!(parser.get("size"), Some("128M".to_owned())); +//! assert_eq!(parser.get("mergeable"), Some("on".to_owned())); +//! ``` + use std::collections::HashMap; use std::fmt::{Display, Write}; use std::num::ParseIntError; @@ -27,6 +50,12 @@ mod private_trait { } use private_trait::Parseable; +/// A parser for comma-separated `key=value` option strings. +/// +/// Options must be registered with [`add`](Self::add) or +/// [`add_valueless`](Self::add_valueless) before parsing. After calling +/// [`parse`](Self::parse), values can be retrieved with [`get`](Self::get) +/// or converted to a specific type with [`convert`](Self::convert). #[derive(Default)] pub struct OptionParser { options: HashMap, @@ -37,14 +66,19 @@ struct OptionParserValue { requires_value: bool, } +/// Errors returned when parsing or converting options. #[derive(Debug, Error)] pub enum OptionParserError { + /// An option name was not previously registered with [`OptionParser::add`]. #[error("unknown option: {0}")] UnknownOption(String), + /// The input string has invalid syntax (unbalanced quotes/brackets, missing `=`). #[error("unknown option: {0}")] InvalidSyntax(String), + /// A value could not be converted to the requested type. #[error("unable to convert {1} for {0}")] Conversion(String /* field */, String /* value */), + /// A value was syntactically valid but semantically wrong. #[error("invalid value: {0}")] InvalidValue(String), } @@ -87,6 +121,7 @@ fn split_commas(s: &str) -> OptionParserResult> { } impl OptionParser { + /// Creates an empty `OptionParser` with no registered options. pub fn new() -> Self { Self { options: HashMap::new(), @@ -122,14 +157,30 @@ impl OptionParser { Ok(()) } + /// Parses a comma-separated `key=value` string, updating registered options. + /// + /// Returns an error if the input contains an unknown option name, has + /// unbalanced quotes or brackets, or a value-requiring option lacks `=`. pub fn parse(&mut self, input: &str) -> OptionParserResult<()> { self.parse_inner(input, false) } + /// Like [`parse`](Self::parse), but silently ignores unknown option names. + /// + /// This is useful when multiple parsers share the same input string and + /// each only cares about a subset of the options. pub fn parse_subset(&mut self, input: &str) -> OptionParserResult<()> { self.parse_inner(input, true) } + /// Registers a named option that requires a value (i.e. `key=value`). + /// + /// Option names must not contain `"`, `[`, `]`, `=`, or `,`. + /// Returns `&mut Self` for chaining. + /// + /// # Panics + /// + /// Panics if the option name contains a forbidden character. pub fn add(&mut self, option: &str) -> &mut Self { // Check that option=value has balanced // quotes and brackets iff value does. @@ -148,6 +199,9 @@ impl OptionParser { self } + /// Registers multiple value-requiring options at once. + /// + /// Equivalent to calling [`add`](Self::add) for each element in the slice. pub fn add_all(&mut self, options: &[&str]) -> &mut Self { for option in options { self.add(option); @@ -156,6 +210,10 @@ impl OptionParser { self } + /// Registers a flag-style option that does not take a value. + /// + /// When this option appears in the input string (without `=`), it is + /// marked as set. Use [`is_set`](Self::is_set) to query it. pub fn add_valueless(&mut self, option: &str) -> &mut Self { self.options.insert( option.to_owned(), @@ -168,6 +226,10 @@ impl OptionParser { self } + /// Returns the raw string value of an option, or `None` if the option was + /// not set or if its value is an empty string (e.g. `key=`). + /// + /// Surrounding double-quotes in the value are removed. pub fn get(&self, option: &str) -> Option { self.options .get(option) @@ -181,6 +243,9 @@ impl OptionParser { }) } + /// Returns `true` if the option was present in the parsed input. + /// + /// This works for both value-requiring and valueless options. pub fn is_set(&self, option: &str) -> bool { self.options .get(option) @@ -188,6 +253,14 @@ impl OptionParser { .is_some() } + /// Retrieves and converts an option value to type `T`. + /// + /// Returns `Ok(None)` if the option was not set or its value is empty. + /// Returns `Err` if the value cannot be converted to `T`. + /// + /// `T` can be any type that implements `FromStr` (e.g. `u32`, `String`), + /// or one of this crate's types such as [`Toggle`], [`IntegerList`], + /// [`Tuple`], or [`StringList`]. pub fn convert(&self, option: &str) -> OptionParserResult> { match self.options.get(option).and_then(|v| v.value.as_ref()) { None => Ok(None), @@ -204,6 +277,9 @@ impl OptionParser { } } +/// A boolean-like value that accepts `"on"`, `"true"`, `"off"`, `"false"`, or `""`. +/// +/// An empty string is treated as `false`. pub struct Toggle(pub bool); #[derive(Error, Debug)] @@ -227,6 +303,10 @@ impl Parseable for Toggle { } } +/// A byte size parsed from a human-readable string with optional `K`, `M`, or `G` suffix. +/// +/// The suffix is binary (1K = 1024, 1M = 1048576, 1G = 1073741824). +/// A bare integer is treated as bytes. pub struct ByteSized(pub u64); #[derive(Error, Debug)] @@ -259,6 +339,9 @@ impl FromStr for ByteSized { } } +/// A list of integers parsed from a bracket-enclosed, comma-separated string. +/// +/// Ranges are supported with `-`: `"[0,2-4,6]"` produces `[0, 2, 3, 4, 6]`. pub struct IntegerList(pub Vec); impl Display for IntegerList { @@ -324,7 +407,11 @@ impl Parseable for IntegerList { } } +/// Types that can appear as the second element of a [`Tuple`] pair. +/// +/// Implemented for `u64`, `Vec`, `Vec`, and `Vec`. pub trait TupleValue { + /// Parses the value portion of a `key@value` tuple element. fn parse_value(input: &str) -> Result where Self: Sized; @@ -366,6 +453,10 @@ impl TupleValue for Vec { } } +/// A list of `key@value` pairs parsed from a bracket-enclosed string. +/// +/// The format is `[key1@value1,key2@value2,...]` where `@` separates each +/// pair's elements. `S` is the key type and `T` is the value type. #[derive(PartialEq, Eq, Debug)] pub struct Tuple(pub Vec<(S, T)>); @@ -421,6 +512,9 @@ impl Parseable for Tuple { } } +/// A list of strings parsed from a bracket-enclosed, comma-separated string. +/// +/// The format is `[str1,str2,...]`. Brackets are optional. #[derive(Default)] pub struct StringList(pub Vec);