#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature {
pub name: String,
pub slot: Option<String>,
pub operands: Vec<Param>,
pub options: Vec<OptSpec>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub name: String,
pub optional: bool,
pub variadic: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OptSpec {
pub long: Option<String>,
pub short: Option<String>,
pub value: Option<String>,
pub required: bool,
}
impl OptSpec {
pub fn answers_to(&self, written: &str) -> bool {
self.long.as_deref() == Some(written) || self.short.as_deref() == Some(written)
}
pub fn spelled(&self) -> String {
let names: Vec<&str> = [self.short.as_deref(), self.long.as_deref()]
.into_iter()
.flatten()
.collect();
let mut s = names.join("|");
if let Some(v) = &self.value {
s.push('=');
s.push_str(v);
if !self.required {
s.push('?');
}
}
s
}
}
impl Signature {
pub fn min_operands(&self) -> usize {
self.operands
.iter()
.filter(|p| !p.optional && !p.variadic)
.count()
}
pub fn max_operands(&self) -> Option<usize> {
if self.operands.iter().any(|p| p.variadic) {
None
} else {
Some(self.operands.len())
}
}
pub fn option(&self, written: &str) -> Option<&OptSpec> {
self.options.iter().find(|o| o.answers_to(written))
}
pub fn text(&self) -> String {
let mut s = self.name.clone();
if let Some(slot) = &self.slot {
s.push('{');
s.push_str(slot);
s.push('}');
}
s.push('(');
let mut fields: Vec<String> = self
.operands
.iter()
.map(|p| {
let mut f = p.name.clone();
if p.variadic {
f.push('…');
} else if p.optional {
f.push('?');
}
f
})
.collect();
if !self.options.is_empty() {
fields.push(
self.options
.iter()
.map(OptSpec::spelled)
.collect::<Vec<_>>()
.join(" "),
);
}
s.push_str(&fields.join("; "));
s.push(')');
s
}
}
pub fn parse_cell(cell: &str) -> Result<Vec<Signature>, String> {
cell.split(" / ").map(parse_form).collect()
}
pub fn parse_form(form: &str) -> Result<Signature, String> {
let form = form.trim();
let name_end = form
.find(|c: char| !(c.is_alphanumeric() || c == '_' || c == ':'))
.unwrap_or(form.len());
let name = &form[..name_end];
if name.is_empty() {
return Err(format!(
"'{form}': a signature opens with the command's name"
));
}
let mut rest = &form[name_end..];
let mut slot = None;
if let Some(after) = rest.strip_prefix('{') {
let Some(close) = after.find('}') else {
return Err(format!("'{form}': the braced slot is not closed"));
};
let word = after[..close].trim();
if word.is_empty() || !word.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(format!("'{form}': the braced slot is one word"));
}
slot = Some(word.to_string());
rest = &after[close + 1..];
}
let Some(inner) = rest.strip_prefix('(').and_then(|r| r.strip_suffix(')')) else {
return Err(format!(
"'{form}': the parentheses are always written, empty for a command without operands"
));
};
let fields: Vec<&str> = inner.split(';').map(str::trim).collect();
let fields: Vec<&str> = if fields == [""] { Vec::new() } else { fields };
let mut operands = Vec::new();
let mut options = Vec::new();
for (i, field) in fields.iter().enumerate() {
if field.is_empty() {
return Err(format!("'{form}': an empty field"));
}
if field.starts_with('-') {
if i + 1 != fields.len() {
return Err(format!("'{form}': the option segment stands last"));
}
for word in field.split_whitespace() {
let opt = parse_option(word).map_err(|e| format!("'{form}': {e}"))?;
for seen in &options {
let seen: &OptSpec = seen;
if seen.long.is_some() && seen.long == opt.long {
return Err(format!(
"'{form}': '{}' is declared twice",
opt.long.as_deref().unwrap_or("")
));
}
if seen.short.is_some() && seen.short == opt.short {
return Err(format!(
"'{form}': the short option '{}' is declared twice",
opt.short.as_deref().unwrap_or("")
));
}
}
options.push(opt);
}
continue;
}
let (text, optional, variadic) = if let Some(t) = field.strip_suffix('…') {
(t, false, true)
} else if let Some(t) = field.strip_suffix('?') {
(t, true, false)
} else {
(*field, false, false)
};
if text.is_empty() || !text.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(format!("'{form}': '{field}' is no operand name"));
}
if operands.iter().any(|p: &Param| p.variadic) {
return Err(format!(
"'{form}': '{field}' follows a '…' operand, which stands last"
));
}
if !optional && !variadic && operands.iter().any(|p: &Param| p.optional) {
return Err(format!(
"'{form}': the required operand '{field}' follows an optional one"
));
}
operands.push(Param {
name: text.to_string(),
optional,
variadic,
});
}
Ok(Signature {
name: name.to_string(),
slot,
operands,
options,
})
}
fn parse_option(word: &str) -> Result<OptSpec, String> {
let (names, value) = match word.find('=') {
Some(eq) => (&word[..eq], Some(&word[eq + 1..])),
None => (word, None),
};
let (value, required) = match value {
None => (None, false),
Some(v) => match v.strip_suffix('?') {
Some(v) => (Some(v), false),
None => (Some(v), true),
},
};
if let Some(v) = value
&& v.is_empty()
{
return Err(format!("'{word}': the value's placeholder is missing"));
}
if value.is_none() && names.ends_with('?') {
return Err(format!(
"'{word}': a flag is always optional, never written with '?'"
));
}
let mut long = None;
let mut short = None;
for name in names.split('|') {
if let Some(l) = name.strip_prefix("--") {
if l.is_empty()
|| !l
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
return Err(format!("'{word}': '{name}' is no option name"));
}
if long.replace(name.to_string()).is_some() {
return Err(format!("'{word}': two long spellings"));
}
} else if let Some(s) = name.strip_prefix('-') {
let mut chars = s.chars();
if !(chars.next().is_some_and(char::is_alphabetic) && chars.next().is_none()) {
return Err(format!(
"'{word}': a short option is one letter, '{name}' given"
));
}
if short.replace(name.to_string()).is_some() {
return Err(format!("'{word}': two short spellings"));
}
} else {
return Err(format!("'{word}': '{name}' is no option"));
}
}
Ok(OptSpec {
long,
short,
value: value.map(str::to_string),
required,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_examples_of_the_notation_parse() {
let s = parse_form("f{a}(b; c?; -k|--kind --from=PATH --weight=UNIT?)").unwrap();
assert_eq!(s.name, "f");
assert_eq!(s.slot.as_deref(), Some("a"));
assert_eq!(s.min_operands(), 1);
assert_eq!(s.max_operands(), Some(2));
assert_eq!(s.options.len(), 3);
let kind = s.option("-k").unwrap();
assert_eq!(kind.long.as_deref(), Some("--kind"));
assert!(kind.value.is_none() && !kind.required);
let from = s.option("--from").unwrap();
assert_eq!(from.value.as_deref(), Some("PATH"));
assert!(from.required && from.short.is_none());
let weight = s.option("--weight").unwrap();
assert!(!weight.required);
assert_eq!(
s.text(),
"f{a}(b; c?; -k|--kind --from=PATH --weight=UNIT?)"
);
let now = parse_form("now()").unwrap();
assert!(now.slot.is_none() && now.operands.is_empty() && now.options.is_empty());
let count = parse_form("count{stream}(-o|--of=expr?)").unwrap();
assert_eq!(count.slot.as_deref(), Some("stream"));
assert_eq!(count.max_operands(), Some(0));
let lev = parse_form("levenshtein{text}(other)").unwrap();
assert_eq!((lev.min_operands(), lev.max_operands()), (1, Some(1)));
let group = parse_form("group{stream}(key; key…)").unwrap();
assert_eq!((group.min_operands(), group.max_operands()), (1, None));
let tree = parse_form(
"tree{context}(expr…; -d|--depth=n? -k|--kind -t|--traits -p|--props -r|--record -s|--sample=n?)",
)
.unwrap();
assert_eq!((tree.min_operands(), tree.max_operands()), (0, None));
assert!(!tree.option("--depth").unwrap().required);
let session = parse_form("session(-p|--pretty=always|structures|off?)").unwrap();
assert_eq!(
session.option("--pretty").unwrap().value.as_deref(),
Some("always|structures|off")
);
assert_eq!(parse_cell("f(a) / f(a; b; c)").unwrap().len(), 2);
}
#[test]
fn the_notation_refuses_what_it_rules_out() {
for (form, why) in [
("round", "parentheses"),
("f{a}", "parentheses"),
("f{}(a)", "one word"),
("f(a?; b)", "follows an optional"),
("f(a…; b)", "stands last"),
("f(-k|--kind; b)", "option segment stands last"),
("f(a; -k|--kind?)", "always optional"),
("f(a; -kk|--kind)", "one letter"),
("f(a; -k|--kind -k|--key)", "declared twice"),
("f(a; --kind --kind=n)", "declared twice"),
("f(a; --from=)", "placeholder"),
("f(a; b;)", "empty field"),
] {
let e = parse_form(form).unwrap_err();
assert!(e.contains(why), "{form}: {e}");
}
}
}