use serde::{Deserialize, Serialize};
pub(crate) const DIRECTIVE_PREFIX: char = ':';
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[non_exhaustive]
pub enum ReplDirective {
#[serde(rename = "repl_help")]
Help,
#[serde(rename = "repl_load")]
Load,
#[serde(rename = "repl_quit")]
Quit,
#[serde(rename = "repl_exit")]
Exit,
}
impl ReplDirective {
pub const ALL: &'static [ReplDirective] = &[
ReplDirective::Help,
ReplDirective::Load,
ReplDirective::Quit,
ReplDirective::Exit,
];
pub fn as_str(self) -> &'static str {
match self {
ReplDirective::Help => "repl_help",
ReplDirective::Load => "repl_load",
ReplDirective::Quit => "repl_quit",
ReplDirective::Exit => "repl_exit",
}
}
pub fn default_name(self) -> &'static str {
match self {
ReplDirective::Help => "help",
ReplDirective::Load => "load",
ReplDirective::Quit => "quit",
ReplDirective::Exit => "exit",
}
}
pub fn default_aliases(self) -> &'static [&'static str] {
match self {
ReplDirective::Help => &["h", "?"],
ReplDirective::Load => &[],
ReplDirective::Quit => &["q"],
ReplDirective::Exit => &[],
}
}
pub fn default_description(self) -> &'static str {
match self {
ReplDirective::Help => "Show the application help, or the help of a command",
ReplDirective::Load => "Run every line of a script file",
ReplDirective::Quit => "Save the session history and leave",
ReplDirective::Exit => "Leave without saving the session history",
}
}
pub fn usage(self) -> &'static str {
match self {
ReplDirective::Help => "[command]",
ReplDirective::Load => "<path>",
ReplDirective::Quit => "",
ReplDirective::Exit => "",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct DirectiveDefinition {
pub implementation: ReplDirective,
pub name: String,
#[serde(default)]
pub aliases: Vec<String>,
pub description: String,
}
impl DirectiveDefinition {
pub fn default_for(directive: ReplDirective) -> Self {
Self {
implementation: directive,
name: directive.default_name().to_string(),
aliases: directive
.default_aliases()
.iter()
.map(|alias| alias.to_string())
.collect(),
description: directive.default_description().to_string(),
}
}
pub(crate) fn usage_line(&self) -> String {
match self.implementation.usage() {
"" => format!("{}{}", DIRECTIVE_PREFIX, self.name),
args => format!("{}{} {}", DIRECTIVE_PREFIX, self.name, args),
}
}
}
pub fn effective_directives(overrides: &[DirectiveDefinition]) -> Vec<DirectiveDefinition> {
ReplDirective::ALL
.iter()
.map(|&directive| {
overrides
.iter()
.rev()
.find(|entry| entry.implementation == directive)
.cloned()
.unwrap_or_else(|| DirectiveDefinition::default_for(directive))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn entry(directive: ReplDirective, name: &str, aliases: &[&str]) -> DirectiveDefinition {
DirectiveDefinition {
implementation: directive,
name: name.to_string(),
aliases: aliases.iter().map(|a| a.to_string()).collect(),
description: format!("custom {}", name),
}
}
#[test]
fn test_all_lists_each_variant_once() {
let unique: HashSet<_> = ReplDirective::ALL.iter().collect();
assert_eq!(unique.len(), ReplDirective::ALL.len());
assert_eq!(ReplDirective::ALL.len(), 4);
}
#[test]
fn test_every_directive_has_default_name_and_description() {
for &directive in ReplDirective::ALL {
assert!(!directive.default_name().is_empty(), "{:?}", directive);
assert!(
!directive.default_description().is_empty(),
"{:?}",
directive
);
assert!(directive.as_str().starts_with("repl_"), "{:?}", directive);
}
}
#[test]
fn test_default_values_per_directive() {
assert_eq!(ReplDirective::Help.default_name(), "help");
assert_eq!(ReplDirective::Help.default_aliases(), &["h", "?"]);
assert_eq!(ReplDirective::Help.usage(), "[command]");
assert_eq!(ReplDirective::Load.default_name(), "load");
assert!(ReplDirective::Load.default_aliases().is_empty());
assert_eq!(ReplDirective::Load.usage(), "<path>");
assert_eq!(ReplDirective::Quit.default_name(), "quit");
assert_eq!(ReplDirective::Quit.default_aliases(), &["q"]);
assert_eq!(ReplDirective::Quit.usage(), "");
assert_eq!(ReplDirective::Exit.default_name(), "exit");
assert!(ReplDirective::Exit.default_aliases().is_empty());
assert_eq!(ReplDirective::Exit.usage(), "");
}
#[test]
fn test_default_names_and_aliases_are_disjoint_and_valid() {
let mut seen = HashSet::new();
for &directive in ReplDirective::ALL {
let tokens = std::iter::once(directive.default_name())
.chain(directive.default_aliases().iter().copied());
for token in tokens {
assert!(!token.is_empty());
assert!(!token.starts_with(':'));
assert!(!token.chars().any(char::is_whitespace));
assert!(seen.insert(token), "'{}' used twice", token);
}
}
}
#[test]
fn test_usage_has_no_surrounding_whitespace() {
for &directive in ReplDirective::ALL {
assert_eq!(directive.usage(), directive.usage().trim());
}
}
#[test]
fn test_default_for_matches_accessors() {
for &directive in ReplDirective::ALL {
let def = DirectiveDefinition::default_for(directive);
assert_eq!(def.implementation, directive);
assert_eq!(def.name, directive.default_name());
assert_eq!(def.aliases, directive.default_aliases());
assert_eq!(def.description, directive.default_description());
}
}
#[test]
fn test_usage_line_defaults() {
let lines: Vec<_> = ReplDirective::ALL
.iter()
.map(|&d| DirectiveDefinition::default_for(d).usage_line())
.collect();
assert_eq!(
lines,
vec![":help [command]", ":load <path>", ":quit", ":exit"]
);
}
#[test]
fn test_usage_line_follows_overridden_name() {
assert_eq!(
entry(ReplDirective::Help, "aide", &[]).usage_line(),
":aide [command]"
);
assert_eq!(
entry(ReplDirective::Exit, "sortir", &[]).usage_line(),
":sortir"
);
}
#[test]
fn test_serde_identifiers_match_as_str() {
for &directive in ReplDirective::ALL {
let yaml = serde_yaml::to_string(&directive).unwrap();
assert_eq!(yaml.trim(), directive.as_str());
let back: ReplDirective = serde_yaml::from_str(directive.as_str()).unwrap();
assert_eq!(back, directive);
}
}
#[test]
fn test_deserialize_definition_aliases_default_empty() {
let yaml = r#"
implementation: repl_exit
name: sortir
description: "Sortir sans enregistrer"
"#;
let def: DirectiveDefinition = serde_yaml::from_str(yaml).unwrap();
assert_eq!(def.implementation, ReplDirective::Exit);
assert_eq!(def.name, "sortir");
assert!(def.aliases.is_empty());
}
#[test]
fn test_deserialize_definition_requires_name_and_description() {
let no_name = "implementation: repl_help\ndescription: x\n";
assert!(serde_yaml::from_str::<DirectiveDefinition>(no_name).is_err());
let no_description = "implementation: repl_help\nname: aide\n";
assert!(serde_yaml::from_str::<DirectiveDefinition>(no_description).is_err());
}
#[test]
fn test_deserialize_unknown_implementation_lists_valid_values() {
let yaml = "implementation: repl_history\nname: hist\ndescription: x\n";
let err = serde_yaml::from_str::<DirectiveDefinition>(yaml)
.unwrap_err()
.to_string();
assert!(err.contains("repl_history"), "{}", err);
for &directive in ReplDirective::ALL {
assert!(err.contains(directive.as_str()), "{}", err);
}
}
#[test]
fn test_effective_directives_without_override() {
let table = effective_directives(&[]);
let expected: Vec<_> = ReplDirective::ALL
.iter()
.map(|&d| DirectiveDefinition::default_for(d))
.collect();
assert_eq!(table, expected);
}
#[test]
fn test_effective_directives_partial_override() {
let help = entry(ReplDirective::Help, "aide", &[]);
let table = effective_directives(std::slice::from_ref(&help));
assert_eq!(table.len(), ReplDirective::ALL.len());
assert_eq!(table[0], help);
assert!(table[0].aliases.is_empty());
for (def, &directive) in table.iter().zip(ReplDirective::ALL).skip(1) {
assert_eq!(*def, DirectiveDefinition::default_for(directive));
}
}
#[test]
fn test_effective_directives_full_override_keeps_all_order() {
let overrides = vec![
entry(ReplDirective::Exit, "sortir", &[]),
entry(ReplDirective::Quit, "quitter", &["q"]),
entry(ReplDirective::Load, "charger", &["c"]),
entry(ReplDirective::Help, "aide", &["a", "?"]),
];
let table = effective_directives(&overrides);
let names: Vec<_> = table.iter().map(|d| d.name.as_str()).collect();
assert_eq!(names, vec!["aide", "charger", "quitter", "sortir"]);
let order: Vec<_> = table.iter().map(|d| d.implementation).collect();
assert_eq!(order, ReplDirective::ALL);
}
#[test]
fn test_effective_directives_last_duplicate_wins() {
let overrides = vec![
entry(ReplDirective::Quit, "first", &[]),
entry(ReplDirective::Quit, "second", &[]),
];
let table = effective_directives(&overrides);
assert_eq!(table[2].name, "second");
}
}