use crate::compat::{String, Vec};
use CommandSignature::PayloadFree;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandEffect {
MutatesControl,
MutatesCollection,
SideEffecting,
}
impl CommandEffect {
pub const fn token(self) -> &'static str {
match self {
Self::MutatesControl => "mutates-control",
Self::MutatesCollection => "mutates-collection",
Self::SideEffecting => "side-effecting",
}
}
pub const fn from_token(token: &str) -> Option<Self> {
match token.as_bytes() {
b"mutates-control" => Some(Self::MutatesControl),
b"mutates-collection" => Some(Self::MutatesCollection),
b"side-effecting" => Some(Self::SideEffecting),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandSignature {
PayloadFree,
OneValue {
parameter: &'static str,
kind: super::types::PropertyValueKind,
},
Composite {
parameter: &'static str,
arity: Option<usize>,
element: Option<super::types::PropertyValueKind>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CommandSchema {
pub signature: CommandSignature,
pub effect: CommandEffect,
}
impl CommandSchema {
pub const fn is_payload_free(&self) -> bool {
matches!(self.signature, CommandSignature::PayloadFree)
}
pub const fn parameter(&self) -> Option<(&'static str, super::types::PropertyValueKind)> {
match self.signature {
CommandSignature::PayloadFree | CommandSignature::Composite { .. } => None,
CommandSignature::OneValue { parameter, kind } => Some((parameter, kind)),
}
}
}
struct VerbClass {
name: &'static str,
signature: CommandSignature,
effect: CommandEffect,
}
const SET_PREFIX: &str = "set_";
static VERB_CLASSES: &[VerbClass] = &[
verb("activate_action", PayloadFree, CommandEffect::MutatesControl),
verb("activate_highlighted", PayloadFree, CommandEffect::MutatesControl),
verb("activate_selected", PayloadFree, CommandEffect::MutatesControl),
verb("back", PayloadFree, CommandEffect::SideEffecting),
verb("cancel_drag", PayloadFree, CommandEffect::MutatesControl),
verb("clear_cache", PayloadFree, CommandEffect::MutatesControl),
verb("clear_focused_node", PayloadFree, CommandEffect::MutatesControl),
verb("clear_focused_row", PayloadFree, CommandEffect::MutatesControl),
verb("clear_selection", PayloadFree, CommandEffect::MutatesControl),
verb("collapse", PayloadFree, CommandEffect::MutatesControl),
verb("expand", PayloadFree, CommandEffect::MutatesControl),
verb("next", PayloadFree, CommandEffect::MutatesControl),
verb("previous", PayloadFree, CommandEffect::MutatesControl),
verb("previous_page", PayloadFree, CommandEffect::MutatesControl),
verb("next_page", PayloadFree, CommandEffect::SideEffecting),
verb("select_all", PayloadFree, CommandEffect::MutatesControl),
verb("clear", PayloadFree, CommandEffect::MutatesControl),
verb("toggle", PayloadFree, CommandEffect::MutatesControl),
verb("toggle_preview_mode", PayloadFree, CommandEffect::MutatesControl),
verb("toggle_shift", PayloadFree, CommandEffect::MutatesControl),
verb("toggle_conjunction", PayloadFree, CommandEffect::MutatesControl),
verb("step_down", PayloadFree, CommandEffect::MutatesControl),
verb("step_up", PayloadFree, CommandEffect::MutatesControl),
verb("dismiss_selected", PayloadFree, CommandEffect::MutatesControl),
verb("zoom", PayloadFree, CommandEffect::MutatesControl),
verb("accept", PayloadFree, CommandEffect::MutatesControl),
verb("click", PayloadFree, CommandEffect::MutatesControl),
verb("complete", PayloadFree, CommandEffect::MutatesControl),
verb("dismiss", PayloadFree, CommandEffect::MutatesControl),
verb("finish", PayloadFree, CommandEffect::MutatesControl),
verb("finish_editing", PayloadFree, CommandEffect::MutatesControl),
verb("start_editing", PayloadFree, CommandEffect::MutatesControl),
verb("reject", PayloadFree, CommandEffect::MutatesControl),
verb("submit", PayloadFree, CommandEffect::MutatesControl),
verb("trigger", PayloadFree, CommandEffect::MutatesControl),
verb("trigger_primary", PayloadFree, CommandEffect::MutatesControl),
verb("undo", PayloadFree, CommandEffect::MutatesControl),
verb("redo", PayloadFree, CommandEffect::MutatesControl),
verb("show_today", PayloadFree, CommandEffect::MutatesControl),
verb("hide", PayloadFree, CommandEffect::MutatesControl),
verb("show", PayloadFree, CommandEffect::MutatesControl),
verb("open", PayloadFree, CommandEffect::MutatesControl),
verb("close", PayloadFree, CommandEffect::MutatesControl),
verb("open_menu", PayloadFree, CommandEffect::MutatesControl),
verb("close_menu", PayloadFree, CommandEffect::MutatesControl),
verb("open_popup", PayloadFree, CommandEffect::MutatesControl),
verb("hide_popup", PayloadFree, CommandEffect::MutatesControl),
verb("show_popup", PayloadFree, CommandEffect::MutatesControl),
verb("play", PayloadFree, CommandEffect::MutatesControl),
verb("pause", PayloadFree, CommandEffect::MutatesControl),
verb("stop", PayloadFree, CommandEffect::MutatesControl),
verb("backspace", PayloadFree, CommandEffect::MutatesControl),
verb("close_find", PayloadFree, CommandEffect::MutatesControl),
verb("copy", PayloadFree, CommandEffect::MutatesControl),
verb("cut", PayloadFree, CommandEffect::MutatesControl),
verb("delete_char", PayloadFree, CommandEffect::MutatesControl),
verb("find", PayloadFree, CommandEffect::MutatesControl),
verb("find_next", PayloadFree, CommandEffect::MutatesControl),
verb("find_previous", PayloadFree, CommandEffect::MutatesControl),
verb("insert_char", PayloadFree, CommandEffect::MutatesControl),
verb("paste", PayloadFree, CommandEffect::MutatesControl),
verb("replace", PayloadFree, CommandEffect::MutatesControl),
verb("pop", PayloadFree, CommandEffect::MutatesCollection),
verb("insert", PayloadFree, CommandEffect::MutatesCollection),
verb("clear_segments", PayloadFree, CommandEffect::MutatesCollection),
verb("add_separator", PayloadFree, CommandEffect::MutatesCollection),
verb("add_overlay", PayloadFree, CommandEffect::MutatesCollection),
verb("clear_model", PayloadFree, CommandEffect::MutatesCollection),
verb("clear_data_source", PayloadFree, CommandEffect::MutatesCollection),
verb("clear_source", PayloadFree, CommandEffect::MutatesCollection),
verb("clear_query", PayloadFree, CommandEffect::MutatesCollection),
verb("scroll_rows", PayloadFree, CommandEffect::MutatesControl),
verb("mark_all_read", PayloadFree, CommandEffect::MutatesCollection),
verb("fetch_visible_rows", PayloadFree, CommandEffect::MutatesControl),
verb("fetch_visible_window", PayloadFree, CommandEffect::MutatesControl),
verb("delete_line", PayloadFree, CommandEffect::MutatesCollection),
verb("duplicate_line", PayloadFree, CommandEffect::MutatesCollection),
verb("indent", PayloadFree, CommandEffect::MutatesCollection),
verb("outdent", PayloadFree, CommandEffect::MutatesCollection),
verb("sort_lines", PayloadFree, CommandEffect::MutatesCollection),
verb("toggle_block_comment", PayloadFree, CommandEffect::MutatesCollection),
verb("toggle_comment", PayloadFree, CommandEffect::MutatesCollection),
verb("fold_all", PayloadFree, CommandEffect::MutatesControl),
verb("unfold_all", PayloadFree, CommandEffect::MutatesControl),
verb("go_back", PayloadFree, CommandEffect::SideEffecting),
verb("go_forward", PayloadFree, CommandEffect::SideEffecting),
verb("reload", PayloadFree, CommandEffect::SideEffecting),
];
const fn verb(name: &'static str, signature: CommandSignature, effect: CommandEffect) -> VerbClass {
VerbClass { name, signature, effect }
}
pub fn command_schema(
name: &str,
properties: &[super::types::PropertySchema],
) -> Option<CommandSchema> {
if let Some(property_name) = name.strip_prefix(SET_PREFIX) {
return properties.iter().find(|schema| schema.name == property_name).map(|schema| {
CommandSchema {
signature: CommandSignature::OneValue {
parameter: schema.name,
kind: schema.value_kind,
},
effect: CommandEffect::MutatesControl,
}
});
}
VERB_CLASSES
.iter()
.find(|class| class.name == name)
.map(|class| CommandSchema { signature: class.signature, effect: class.effect })
}
pub fn is_described_command(name: &str, properties: &[super::types::PropertySchema]) -> bool {
command_schema(name, properties).is_some()
}
pub fn described_verbs() -> Vec<&'static str> {
VERB_CLASSES.iter().map(|class| class.name).collect()
}
pub fn effect_tokens() -> Vec<&'static str> {
vec![
CommandEffect::MutatesControl.token(),
CommandEffect::MutatesCollection.token(),
CommandEffect::SideEffecting.token(),
]
}
pub fn describe_command(name: &str, schema: &CommandSchema) -> String {
match schema.signature {
CommandSignature::PayloadFree => {
crate::compat::format!("{name}() — {}", schema.effect.token())
}
CommandSignature::OneValue { parameter, kind } => crate::compat::format!(
"{name}({parameter}: {}) — {}",
super::designer_manifest::value_kind_token(kind),
schema.effect.token()
),
CommandSignature::Composite { parameter, arity, element } => {
let elements = match element {
Some(kind) => crate::compat::format!(
" of {}",
super::designer_manifest::value_kind_token(kind)
),
None => String::new(),
};
match arity {
Some(count) => crate::compat::format!(
"{name}({parameter}: {count} values{elements}) — {}",
schema.effect.token()
),
None => crate::compat::format!(
"{name}({parameter}: list{elements}) — {}",
schema.effect.token()
),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::capability::types::PropertySchema;
#[test]
fn a_set_command_takes_its_property_kind() {
static PROPS: &[PropertySchema] = &[PropertySchema::new(
"value",
super::super::types::PropertyValueKind::UInt,
true,
true,
)];
let schema = command_schema("set_value", PROPS).expect("set_value is described");
assert_eq!(
schema.parameter(),
Some(("value", super::super::types::PropertyValueKind::UInt))
);
assert!(!schema.is_payload_free());
}
#[test]
fn a_set_command_for_a_missing_property_is_not_described() {
static PROPS: &[PropertySchema] = &[PropertySchema::new(
"text",
super::super::types::PropertyValueKind::String,
true,
true,
)];
assert!(command_schema("set_turbo", PROPS).is_none());
}
#[test]
fn verbs_are_described_from_the_table_only() {
static NO_PROPS: &[PropertySchema] = &[];
let toggle = command_schema("toggle", NO_PROPS).expect("toggle is a known verb");
assert!(toggle.is_payload_free());
assert_eq!(toggle.effect, CommandEffect::MutatesControl);
assert!(command_schema("frobnicate_the_widget", NO_PROPS).is_none());
}
#[test]
fn effects_round_trip() {
for effect in [
CommandEffect::MutatesControl,
CommandEffect::MutatesCollection,
CommandEffect::SideEffecting,
] {
assert_eq!(CommandEffect::from_token(effect.token()), Some(effect));
}
assert_eq!(CommandEffect::from_token("no_such_effect"), None);
}
#[test]
fn the_verb_table_has_no_duplicates() {
let mut names: Vec<&str> = VERB_CLASSES.iter().map(|class| class.name).collect();
names.sort_unstable();
let before = names.len();
names.dedup();
assert_eq!(before, names.len(), "the verb table declares a verb twice: {names:?}");
}
#[test]
fn no_verb_uses_the_set_prefix() {
for class in VERB_CLASSES {
assert!(
!class.name.starts_with(SET_PREFIX),
"`{}` belongs in the derived route, not the verb table",
class.name
);
}
}
#[test]
fn describe_renders_both_shapes() {
static PROPS: &[PropertySchema] = &[PropertySchema::new(
"text",
super::super::types::PropertyValueKind::String,
true,
true,
)];
let one =
describe_command("set_text", &command_schema("set_text", PROPS).expect("set_text"));
assert!(one.contains("set_text(text: string)"), "{one}");
let none = describe_command("toggle", &command_schema("toggle", &[]).expect("toggle"));
assert!(none.contains("toggle()"), "{none}");
}
}