option_parser: Split Tuple into Tuple and TupleList

We split the existing `Tuple` type into two types, one that represents
a single `Tuple` and one that represents a list of tuples. This allows
us to test tuple parsing and the parsing of tuple lists via distinct
unit tests in follow-up commits.

The renaming also brings consistency by adapting the naming scheme of
the other list types defined in `option_parser`, e.g. `IntegerList`
and `StringList`.

Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
On-behalf-of: SAP pascal.scholz@sap.com
This commit is contained in:
Pascal Scholz
2026-07-09 09:44:28 +02:00
committed by Rob Bradford
parent fee153841b
commit 8b6f4b0892
2 changed files with 72 additions and 61 deletions

View File

@@ -272,7 +272,7 @@ impl OptionParser {
/// ///
/// `T` can be any type that implements `FromStr` (e.g. `u32`, `String`), /// `T` can be any type that implements `FromStr` (e.g. `u32`, `String`),
/// or one of this crate's types such as [`Toggle`], [`IntegerList`], /// or one of this crate's types such as [`Toggle`], [`IntegerList`],
/// [`Tuple`], or [`StringList`]. /// [`Tuple`], [`TupleList`] or [`StringList`].
pub fn convert<T: Parseable>(&self, option: &str) -> OptionParserResult<Option<T>> { pub fn convert<T: Parseable>(&self, option: &str) -> OptionParserResult<Option<T>> {
match self.options.get(option).and_then(|v| v.value.as_ref()) { match self.options.get(option).and_then(|v| v.value.as_ref()) {
None => Ok(None), None => Ok(None),
@@ -464,62 +464,76 @@ impl TupleValue for Vec<usize> {
} }
} }
/// 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<S, T>(pub Vec<(S, T)>);
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum TupleError { pub enum TupleError {
#[error("invalid value: {0}")] #[error("invalid value: {0}")]
InvalidValue(String), InvalidValue(String),
#[error("split outside brackets")] #[error("Unbalanced brackets in one of the values")]
SplitOutsideBrackets(#[source] OptionParserError), SplitInsideBrackets(#[source] OptionParserError),
#[error("Expected a single pair of enclosing brackets in input: {0}")]
UnbalancedOutsideBrackets(String),
#[error("invalid integer list")] #[error("invalid integer list")]
InvalidIntegerList(#[source] IntegerListParseError), InvalidIntegerList(#[source] IntegerListParseError),
#[error("invalid integer")] #[error("invalid integer")]
InvalidInteger(#[source] ParseIntError), InvalidInteger(#[source] ParseIntError),
} }
/// A tuple consisting of a `key@value` pair parsed from a string.
#[derive(PartialEq, Eq, Debug)]
pub struct Tuple<S, T>(pub S, pub T);
/// 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 TupleList<S, T>(pub Vec<Tuple<S, T>>);
impl<S: Parseable, T: TupleValue> Parseable for Tuple<S, T> { impl<S: Parseable, T: TupleValue> Parseable for Tuple<S, T> {
type Err = TupleError; type Err = TupleError;
fn from_str(tuple: &str) -> result::Result<Self, Self::Err> {
let mut in_quotes = false;
let mut last_idx = 0;
let mut first_val = None;
for (idx, c) in tuple.as_bytes().iter().enumerate() {
match c {
b'"' => in_quotes = !in_quotes,
b'@' if !in_quotes => {
if last_idx != 0 {
return Err(TupleError::InvalidValue((*tuple).to_string()));
}
first_val = Some(&tuple[last_idx..idx]);
last_idx = idx + 1;
}
_ => {}
}
}
let item1 = <S as Parseable>::from_str(
first_val.ok_or(TupleError::InvalidValue((*tuple).to_string()))?,
)
.map_err(|_| TupleError::InvalidValue(first_val.unwrap().to_owned()))?;
let item2: T = TupleValue::parse_value(&tuple[last_idx..])?;
Ok(Tuple(item1, item2))
}
}
impl<S: Parseable, T: TupleValue> Parseable for TupleList<S, T> {
type Err = TupleError;
fn from_str(s: &str) -> result::Result<Self, Self::Err> { fn from_str(s: &str) -> result::Result<Self, Self::Err> {
let mut list: Vec<(S, T)> = Vec::new(); let mut list: Vec<Tuple<S, T>> = Vec::new();
let body = s let body = s
.trim() .trim()
.strip_prefix('[') .strip_prefix('[')
.and_then(|s| s.strip_suffix(']')) .and_then(|s| s.strip_suffix(']'))
.ok_or_else(|| TupleError::InvalidValue(s.to_string()))?; .ok_or_else(|| TupleError::UnbalancedOutsideBrackets(s.to_string()))?;
let tuples_list = split_commas(body).map_err(TupleError::SplitOutsideBrackets)?; let tuples_raw = split_commas(body).map_err(TupleError::SplitInsideBrackets)?;
for tuple in tuples_list.iter() { for tuple_raw in tuples_raw.iter() {
let mut in_quotes = false; list.push(Tuple::from_str(tuple_raw)?);
let mut last_idx = 0;
let mut first_val = None;
for (idx, c) in tuple.as_bytes().iter().enumerate() {
match c {
b'"' => in_quotes = !in_quotes,
b'@' if !in_quotes => {
if last_idx != 0 {
return Err(TupleError::InvalidValue((*tuple).to_string()));
}
first_val = Some(&tuple[last_idx..idx]);
last_idx = idx + 1;
}
_ => {}
}
}
let item1 = <S as Parseable>::from_str(
first_val.ok_or(TupleError::InvalidValue((*tuple).to_string()))?,
)
.map_err(|_| TupleError::InvalidValue(first_val.unwrap().to_owned()))?;
let item2 = TupleValue::parse_value(&tuple[last_idx..])?;
list.push((item1, item2));
} }
Ok(Tuple(list)) Ok(TupleList(list))
} }
} }
@@ -636,10 +650,10 @@ mod unit_tests {
parser.parse("topology=[\"@\"\"b\"@[1,2]]").unwrap(); parser.parse("topology=[\"@\"\"b\"@[1,2]]").unwrap();
assert_eq!( assert_eq!(
parser parser
.convert::<Tuple<String, Vec<u8>>>("topology") .convert::<TupleList<String, Vec<u8>>>("topology")
.unwrap() .unwrap()
.unwrap(), .unwrap(),
Tuple(vec![("@\"b".to_owned(), vec![1, 2])]) TupleList(vec![Tuple("@\"b".to_owned(), vec![1, 2])])
); );
parser.parse("cmdline=\"console=ttyS0,9600n8\"").unwrap(); parser.parse("cmdline=\"console=ttyS0,9600n8\"").unwrap();
@@ -836,30 +850,27 @@ mod unit_tests {
#[test] #[test]
fn test_tuple_single_pair() { fn test_tuple_single_pair() {
let t = Tuple::<String, u64>::from_str("[foo@42]").unwrap(); let t = Tuple::<String, u64>::from_str("foo@42").unwrap();
assert_eq!(t, Tuple(vec![("foo".to_owned(), 42)])); assert_eq!(t, Tuple("foo".to_owned(), 42));
let t = Tuple::<String, Vec<u64>>::from_str("foo@[42]").unwrap();
assert_eq!(t, Tuple("foo".to_owned(), vec![42]));
} }
#[test] #[test]
fn test_tuple_multiple_pairs() { fn test_tuple_list_multiple_pairs() {
let t = Tuple::<String, Vec<u64>>::from_str("[a@[1,2],b@[3,4]]").unwrap(); let t = TupleList::<String, Vec<u64>>::from_str("[a@[1,2],b@[3,4]]").unwrap();
assert_eq!( assert_eq!(
t, t,
Tuple(vec![ TupleList(vec![
("a".to_owned(), vec![1, 2]), Tuple("a".to_owned(), vec![1, 2]),
("b".to_owned(), vec![3, 4]), Tuple("b".to_owned(), vec![3, 4]),
]) ])
); );
} }
#[test] #[test]
fn test_tuple_missing_at_separator() { fn test_tuple_missing_at_separator() {
Tuple::<String, u64>::from_str("[foo42]").unwrap_err(); Tuple::<String, u64>::from_str("foo42").unwrap_err();
}
#[test]
fn test_tuple_missing_brackets() {
Tuple::<String, u64>::from_str("foo@42").unwrap_err();
} }
#[test] #[test]

View File

@@ -16,7 +16,7 @@ use block::ImageType;
use clap::ArgMatches; use clap::ArgMatches;
use log::{debug, warn}; use log::{debug, warn};
use option_parser::{ use option_parser::{
ByteSized, IntegerList, OptionParser, OptionParserError, StringList, Toggle, Tuple, ByteSized, IntegerList, OptionParser, OptionParserError, StringList, Toggle, Tuple, TupleList,
}; };
use pci::NUM_DEVICE_IDS; use pci::NUM_DEVICE_IDS;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -744,11 +744,11 @@ impl CpusConfig {
.map_err(Error::ParseCpus)? .map_err(Error::ParseCpus)?
.unwrap_or(DEFAULT_MAX_PHYS_BITS); .unwrap_or(DEFAULT_MAX_PHYS_BITS);
let affinity = parser let affinity = parser
.convert::<Tuple<u32, Vec<usize>>>("affinity") .convert::<TupleList<u32, Vec<usize>>>("affinity")
.map_err(Error::ParseCpus)? .map_err(Error::ParseCpus)?
.map(|v| { .map(|v| {
v.0.iter() v.0.iter()
.map(|(e1, e2)| CpuAffinity { .map(|Tuple(e1, e2)| CpuAffinity {
vcpu: *e1, vcpu: *e1,
host_cpus: e2.clone().into_boxed_slice(), host_cpus: e2.clone().into_boxed_slice(),
}) })
@@ -1529,11 +1529,11 @@ impl DiskConfig {
.unwrap_or_default(); .unwrap_or_default();
let serial = parser.get("serial"); let serial = parser.get("serial");
let queue_affinity = parser let queue_affinity = parser
.convert::<Tuple<u16, Vec<usize>>>("queue_affinity") .convert::<TupleList<u16, Vec<usize>>>("queue_affinity")
.map_err(Error::ParseDisk)? .map_err(Error::ParseDisk)?
.map(|v| { .map(|v| {
v.0.iter() v.0.iter()
.map(|(e1, e2)| VirtQueueAffinity { .map(|Tuple(e1, e2)| VirtQueueAffinity {
queue_index: *e1, queue_index: *e1,
host_cpus: e2.clone().into_boxed_slice(), host_cpus: e2.clone().into_boxed_slice(),
}) })
@@ -2654,11 +2654,11 @@ impl NumaConfig {
.map_err(Error::ParseNuma)? .map_err(Error::ParseNuma)?
.map(|v| v.0.iter().map(|e| *e as u32).collect()); .map(|v| v.0.iter().map(|e| *e as u32).collect());
let distances = parser let distances = parser
.convert::<Tuple<u64, u64>>("distances") .convert::<TupleList<u64, u64>>("distances")
.map_err(Error::ParseNuma)? .map_err(Error::ParseNuma)?
.map(|v| { .map(|v| {
v.0.iter() v.0.iter()
.map(|(e1, e2)| NumaDistance { .map(|Tuple(e1, e2)| NumaDistance {
destination: *e1 as u32, destination: *e1 as u32,
distance: *e2 as u8, distance: *e2 as u8,
}) })
@@ -2850,11 +2850,11 @@ impl RestoreConfig {
.map_err(Error::ParseRestore)? .map_err(Error::ParseRestore)?
.unwrap_or_default(); .unwrap_or_default();
let net_fds = parser let net_fds = parser
.convert::<Tuple<String, Vec<u64>>>("net_fds") .convert::<TupleList<String, Vec<u64>>>("net_fds")
.map_err(Error::ParseRestore)? .map_err(Error::ParseRestore)?
.map(|v| { .map(|v| {
v.0.iter() v.0.iter()
.map(|(id, fds)| RestoredNetConfig { .map(|Tuple(id, fds)| RestoredNetConfig {
id: id.clone(), id: id.clone(),
num_fds: fds.len(), num_fds: fds.len(),
fds: Some(fds.iter().map(|e| *e as i32).collect()), fds: Some(fds.iter().map(|e| *e as i32).collect()),