use crate::ast::{Arg, FnCall, Operand};
use crate::value::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pipe {
Plain,
Agg,
Both,
}
impl Pipe {
pub fn admits(self, agg: bool) -> bool {
match self {
Pipe::Plain => !agg,
Pipe::Agg => agg,
Pipe::Both => true,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct OperandSpec {
pub name: &'static str,
pub doc: &'static str,
pub values: &'static [&'static str],
pub required: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct OptionSpec {
pub short: Option<&'static str>,
pub long: &'static str,
pub takes_value: bool,
pub doc: &'static str,
}
impl OptionSpec {
fn matches(&self, written: &str) -> bool {
written.strip_prefix("--") == Some(self.long) || self.short == Some(written)
}
fn spelling(&self) -> String {
let mut s = String::new();
if let Some(short) = self.short {
s.push_str(short);
s.push('/');
}
s.push_str("--");
s.push_str(self.long);
if self.takes_value {
s.push_str("=…");
}
s
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputSlot {
Operand(usize),
OperandOrSelf(usize),
Option(&'static str),
}
impl InputSlot {
pub fn operand_index(&self) -> Option<usize> {
match self {
InputSlot::Operand(i) | InputSlot::OperandOrSelf(i) => Some(*i),
InputSlot::Option(_) => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Synopsis {
pub name: &'static str,
pub doc: &'static str,
pub pipes: Pipe,
pub operands: &'static [OperandSpec],
pub options: &'static [OptionSpec],
pub input: InputSlot,
pub returns: &'static str,
}
impl Synopsis {
pub fn variadic(&self) -> bool {
self.operands.last().is_some_and(|o| o.name == "...")
}
pub fn open(&self) -> bool {
self.options.iter().any(|o| o.long == "*")
}
pub fn render(&self, prefix: &str) -> String {
let pipe = match self.pipes {
Pipe::Plain => "|",
Pipe::Agg => "@|",
Pipe::Both => "| or @|",
};
let mut fields: Vec<String> = Vec::new();
for (i, op) in self.operands.iter().enumerate() {
if op.name == "..." {
fields.push("…".to_string());
continue;
}
let slot = self.input.operand_index() == Some(i);
let mut f = if slot {
format!("[{} = $_]", op.name)
} else if op.required {
op.name.to_string()
} else {
format!("[{}]", op.name)
};
if !op.values.is_empty() {
f.push_str(&format!(" ({})", op.values.join("|")));
}
fields.push(f);
}
if !self.options.is_empty() {
fields.push(
self.options
.iter()
.map(|o| {
if o.long == "*" {
return "--…".to_string();
}
let mut s = o.spelling();
if self.input == InputSlot::Option(o.long) {
s.push_str(" = $_");
}
s
})
.collect::<Vec<_>>()
.join(" "),
);
}
format!(
"{pipe} {prefix}:{}({}) → {}",
self.name,
fields.join("; "),
self.returns
)
}
}
#[derive(Debug, Clone)]
pub struct LibCall {
pub agg: bool,
pub input: Option<Value>,
pub operands: Vec<Value>,
pub options: Vec<(String, Option<Value>)>,
}
#[derive(Debug, Clone)]
pub enum LibFailure {
Failed(String),
Unknown(String),
}
impl LibFailure {
pub fn message(&self) -> &str {
match self {
LibFailure::Failed(m) | LibFailure::Unknown(m) => m,
}
}
}
pub trait CommandLibrary: Send + Sync {
fn prefix(&self) -> &str;
fn commands(&self) -> &[Synopsis];
fn call(&self, name: &str, call: LibCall) -> Result<Value, LibFailure>;
fn requirements(&self) -> &[&str] {
&[]
}
fn available(&self) -> Result<(), String> {
Ok(())
}
fn synopsis(&self, name: &str) -> Option<&Synopsis> {
self.commands().iter().find(|s| s.name == name)
}
fn resolve(&self, name: &str) -> Result<&Synopsis, String> {
self.synopsis(name).ok_or_else(|| {
let names: Vec<&str> = self.commands().iter().map(|s| s.name).collect();
format!(
"library '{}' has no command '{name}'; its commands: {}",
self.prefix(),
names.join(", ")
)
})
}
}
pub fn places_input(call: &FnCall) -> bool {
call.args.iter().any(|a| match a {
Arg::Expr(Operand::Topic) => true,
Arg::Opt {
value: Some(Operand::Topic),
..
} => true,
_ => false,
})
}
pub fn check(lib: &dyn CommandLibrary, name: &str, call: &FnCall, agg: bool) -> Result<(), String> {
let prefix = lib.prefix();
let syn = lib.resolve(name)?;
let shown = syn.render(prefix);
if !syn.pipes.admits(agg) {
return Err(format!(
"'{prefix}:{name}' rides {}; written after '{}'; the synopsis: {shown}",
match syn.pipes {
Pipe::Plain => "the plain pipe",
Pipe::Agg => "the aggregation pipe",
Pipe::Both => "either pipe",
},
if agg { "@|" } else { "|" }
));
}
let written_operands = call
.args
.iter()
.filter(|a| !matches!(a, Arg::Opt { .. }))
.count();
let fixed = syn.operands.len() - usize::from(syn.variadic());
let slot_takes = match syn.input {
InputSlot::Operand(_) => !places_input(call),
InputSlot::OperandOrSelf(_) => !places_input(call) && written_operands < fixed,
InputSlot::Option(_) => false,
};
let room = fixed.saturating_sub(usize::from(slot_takes));
if written_operands > room && !syn.variadic() {
return Err(format!(
"'{prefix}:{name}' takes {room} operand{}, {written_operands} given; the synopsis: {shown}",
if room == 1 { "" } else { "s" }
));
}
let required = syn
.operands
.iter()
.enumerate()
.filter(|(i, op)| {
op.required && op.name != "..." && !(slot_takes && syn.input.operand_index() == Some(*i))
})
.count();
if written_operands < required {
return Err(format!(
"'{prefix}:{name}' needs {required} operand{}, {written_operands} given; the synopsis: {shown}",
if required == 1 { "" } else { "s" }
));
}
for arg in &call.args {
if let Arg::Opt { name: opt, value } = arg {
let Some(spec) = syn.options.iter().find(|o| o.matches(opt)) else {
if syn.open() {
continue;
}
return Err(format!(
"'{prefix}:{name}' has no option '{opt}'; the synopsis: {shown}"
));
};
if spec.takes_value && value.is_none() {
return Err(format!(
"option '{opt}' of '{prefix}:{name}' takes a value ('{opt}=…'); the synopsis: {shown}"
));
}
if !spec.takes_value && value.is_some() {
return Err(format!(
"option '{opt}' of '{prefix}:{name}' is a switch and takes no value; the synopsis: {shown}"
));
}
}
}
Ok(())
}
pub fn canonical_options(
syn: &Synopsis,
written: Vec<(String, Option<Value>)>,
) -> Vec<(String, Option<Value>)> {
written
.into_iter()
.map(|(name, value)| {
let long = syn
.options
.iter()
.find(|o| o.matches(&name))
.map(|o| o.long.to_string())
.unwrap_or_else(|| name.trim_start_matches('-').to_string());
(long, value)
})
.collect()
}