1use serde::{Deserialize, Serialize};
58
59pub(crate) const DIRECTIVE_PREFIX: char = ':';
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
81#[non_exhaustive]
82pub enum ReplDirective {
83 #[serde(rename = "repl_help")]
85 Help,
86
87 #[serde(rename = "repl_load")]
89 Load,
90
91 #[serde(rename = "repl_quit")]
93 Quit,
94
95 #[serde(rename = "repl_exit")]
97 Exit,
98}
99
100impl ReplDirective {
101 pub const ALL: &'static [ReplDirective] = &[
104 ReplDirective::Help,
105 ReplDirective::Load,
106 ReplDirective::Quit,
107 ReplDirective::Exit,
108 ];
109
110 pub fn as_str(self) -> &'static str {
112 match self {
113 ReplDirective::Help => "repl_help",
114 ReplDirective::Load => "repl_load",
115 ReplDirective::Quit => "repl_quit",
116 ReplDirective::Exit => "repl_exit",
117 }
118 }
119
120 pub fn default_name(self) -> &'static str {
122 match self {
123 ReplDirective::Help => "help",
124 ReplDirective::Load => "load",
125 ReplDirective::Quit => "quit",
126 ReplDirective::Exit => "exit",
127 }
128 }
129
130 pub fn default_aliases(self) -> &'static [&'static str] {
132 match self {
133 ReplDirective::Help => &["h", "?"],
134 ReplDirective::Load => &[],
135 ReplDirective::Quit => &["q"],
136 ReplDirective::Exit => &[],
137 }
138 }
139
140 pub fn default_description(self) -> &'static str {
143 match self {
144 ReplDirective::Help => "Show the application help, or the help of a command",
145 ReplDirective::Load => "Run every line of a script file",
146 ReplDirective::Quit => "Save the session history and leave",
147 ReplDirective::Exit => "Leave without saving the session history",
148 }
149 }
150
151 pub fn usage(self) -> &'static str {
157 match self {
158 ReplDirective::Help => "[command]",
159 ReplDirective::Load => "<path>",
160 ReplDirective::Quit => "",
161 ReplDirective::Exit => "",
162 }
163 }
164}
165
166#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
180pub struct DirectiveDefinition {
181 pub implementation: ReplDirective,
183
184 pub name: String,
186
187 #[serde(default)]
189 pub aliases: Vec<String>,
190
191 pub description: String,
193}
194
195impl DirectiveDefinition {
196 pub fn default_for(directive: ReplDirective) -> Self {
209 Self {
210 implementation: directive,
211 name: directive.default_name().to_string(),
212 aliases: directive
213 .default_aliases()
214 .iter()
215 .map(|alias| alias.to_string())
216 .collect(),
217 description: directive.default_description().to_string(),
218 }
219 }
220
221 pub(crate) fn usage_line(&self) -> String {
227 match self.implementation.usage() {
228 "" => format!("{}{}", DIRECTIVE_PREFIX, self.name),
229 args => format!("{}{} {}", DIRECTIVE_PREFIX, self.name, args),
230 }
231 }
232}
233
234pub fn effective_directives(overrides: &[DirectiveDefinition]) -> Vec<DirectiveDefinition> {
261 ReplDirective::ALL
262 .iter()
263 .map(|&directive| {
264 overrides
265 .iter()
266 .rev()
267 .find(|entry| entry.implementation == directive)
268 .cloned()
269 .unwrap_or_else(|| DirectiveDefinition::default_for(directive))
270 })
271 .collect()
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use std::collections::HashSet;
278
279 fn entry(directive: ReplDirective, name: &str, aliases: &[&str]) -> DirectiveDefinition {
280 DirectiveDefinition {
281 implementation: directive,
282 name: name.to_string(),
283 aliases: aliases.iter().map(|a| a.to_string()).collect(),
284 description: format!("custom {}", name),
285 }
286 }
287
288 #[test]
293 fn test_all_lists_each_variant_once() {
294 let unique: HashSet<_> = ReplDirective::ALL.iter().collect();
295 assert_eq!(unique.len(), ReplDirective::ALL.len());
296 assert_eq!(ReplDirective::ALL.len(), 4);
297 }
298
299 #[test]
300 fn test_every_directive_has_default_name_and_description() {
301 for &directive in ReplDirective::ALL {
302 assert!(!directive.default_name().is_empty(), "{:?}", directive);
303 assert!(
304 !directive.default_description().is_empty(),
305 "{:?}",
306 directive
307 );
308 assert!(directive.as_str().starts_with("repl_"), "{:?}", directive);
309 }
310 }
311
312 #[test]
313 fn test_default_values_per_directive() {
314 assert_eq!(ReplDirective::Help.default_name(), "help");
315 assert_eq!(ReplDirective::Help.default_aliases(), &["h", "?"]);
316 assert_eq!(ReplDirective::Help.usage(), "[command]");
317
318 assert_eq!(ReplDirective::Load.default_name(), "load");
319 assert!(ReplDirective::Load.default_aliases().is_empty());
320 assert_eq!(ReplDirective::Load.usage(), "<path>");
321
322 assert_eq!(ReplDirective::Quit.default_name(), "quit");
323 assert_eq!(ReplDirective::Quit.default_aliases(), &["q"]);
324 assert_eq!(ReplDirective::Quit.usage(), "");
325
326 assert_eq!(ReplDirective::Exit.default_name(), "exit");
327 assert!(ReplDirective::Exit.default_aliases().is_empty());
328 assert_eq!(ReplDirective::Exit.usage(), "");
329 }
330
331 #[test]
332 fn test_default_names_and_aliases_are_disjoint_and_valid() {
333 let mut seen = HashSet::new();
334 for &directive in ReplDirective::ALL {
335 let tokens = std::iter::once(directive.default_name())
336 .chain(directive.default_aliases().iter().copied());
337 for token in tokens {
338 assert!(!token.is_empty());
339 assert!(!token.starts_with(':'));
340 assert!(!token.chars().any(char::is_whitespace));
341 assert!(seen.insert(token), "'{}' used twice", token);
342 }
343 }
344 }
345
346 #[test]
347 fn test_usage_has_no_surrounding_whitespace() {
348 for &directive in ReplDirective::ALL {
349 assert_eq!(directive.usage(), directive.usage().trim());
350 }
351 }
352
353 #[test]
354 fn test_default_for_matches_accessors() {
355 for &directive in ReplDirective::ALL {
356 let def = DirectiveDefinition::default_for(directive);
357 assert_eq!(def.implementation, directive);
358 assert_eq!(def.name, directive.default_name());
359 assert_eq!(def.aliases, directive.default_aliases());
360 assert_eq!(def.description, directive.default_description());
361 }
362 }
363
364 #[test]
365 fn test_usage_line_defaults() {
366 let lines: Vec<_> = ReplDirective::ALL
367 .iter()
368 .map(|&d| DirectiveDefinition::default_for(d).usage_line())
369 .collect();
370 assert_eq!(
371 lines,
372 vec![":help [command]", ":load <path>", ":quit", ":exit"]
373 );
374 }
375
376 #[test]
377 fn test_usage_line_follows_overridden_name() {
378 assert_eq!(
379 entry(ReplDirective::Help, "aide", &[]).usage_line(),
380 ":aide [command]"
381 );
382 assert_eq!(
383 entry(ReplDirective::Exit, "sortir", &[]).usage_line(),
384 ":sortir"
385 );
386 }
387
388 #[test]
393 fn test_serde_identifiers_match_as_str() {
394 for &directive in ReplDirective::ALL {
395 let yaml = serde_yaml::to_string(&directive).unwrap();
396 assert_eq!(yaml.trim(), directive.as_str());
397 let back: ReplDirective = serde_yaml::from_str(directive.as_str()).unwrap();
398 assert_eq!(back, directive);
399 }
400 }
401
402 #[test]
403 fn test_deserialize_definition_aliases_default_empty() {
404 let yaml = r#"
405implementation: repl_exit
406name: sortir
407description: "Sortir sans enregistrer"
408"#;
409 let def: DirectiveDefinition = serde_yaml::from_str(yaml).unwrap();
410 assert_eq!(def.implementation, ReplDirective::Exit);
411 assert_eq!(def.name, "sortir");
412 assert!(def.aliases.is_empty());
413 }
414
415 #[test]
416 fn test_deserialize_definition_requires_name_and_description() {
417 let no_name = "implementation: repl_help\ndescription: x\n";
418 assert!(serde_yaml::from_str::<DirectiveDefinition>(no_name).is_err());
419 let no_description = "implementation: repl_help\nname: aide\n";
420 assert!(serde_yaml::from_str::<DirectiveDefinition>(no_description).is_err());
421 }
422
423 #[test]
424 fn test_deserialize_unknown_implementation_lists_valid_values() {
425 let yaml = "implementation: repl_history\nname: hist\ndescription: x\n";
426 let err = serde_yaml::from_str::<DirectiveDefinition>(yaml)
427 .unwrap_err()
428 .to_string();
429 assert!(err.contains("repl_history"), "{}", err);
430 for &directive in ReplDirective::ALL {
431 assert!(err.contains(directive.as_str()), "{}", err);
432 }
433 }
434
435 #[test]
440 fn test_effective_directives_without_override() {
441 let table = effective_directives(&[]);
442 let expected: Vec<_> = ReplDirective::ALL
443 .iter()
444 .map(|&d| DirectiveDefinition::default_for(d))
445 .collect();
446 assert_eq!(table, expected);
447 }
448
449 #[test]
450 fn test_effective_directives_partial_override() {
451 let help = entry(ReplDirective::Help, "aide", &[]);
452 let table = effective_directives(std::slice::from_ref(&help));
453
454 assert_eq!(table.len(), ReplDirective::ALL.len());
455 assert_eq!(table[0], help);
456 assert!(table[0].aliases.is_empty());
458 for (def, &directive) in table.iter().zip(ReplDirective::ALL).skip(1) {
459 assert_eq!(*def, DirectiveDefinition::default_for(directive));
460 }
461 }
462
463 #[test]
464 fn test_effective_directives_full_override_keeps_all_order() {
465 let overrides = vec![
467 entry(ReplDirective::Exit, "sortir", &[]),
468 entry(ReplDirective::Quit, "quitter", &["q"]),
469 entry(ReplDirective::Load, "charger", &["c"]),
470 entry(ReplDirective::Help, "aide", &["a", "?"]),
471 ];
472 let table = effective_directives(&overrides);
473
474 let names: Vec<_> = table.iter().map(|d| d.name.as_str()).collect();
475 assert_eq!(names, vec!["aide", "charger", "quitter", "sortir"]);
476 let order: Vec<_> = table.iter().map(|d| d.implementation).collect();
477 assert_eq!(order, ReplDirective::ALL);
478 }
479
480 #[test]
481 fn test_effective_directives_last_duplicate_wins() {
482 let overrides = vec![
483 entry(ReplDirective::Quit, "first", &[]),
484 entry(ReplDirective::Quit, "second", &[]),
485 ];
486 let table = effective_directives(&overrides);
487 assert_eq!(table[2].name, "second");
488 }
489}