use crate::compat::{String, Vec};
use super::types::PropertyValueKind::{Bool, Float, String as KindString, UInt};
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("activate_subwindow", PayloadFree, CommandEffect::MutatesControl),
verb("back", PayloadFree, CommandEffect::SideEffecting),
verb("cancel_drag", PayloadFree, CommandEffect::MutatesControl),
verb("choose", 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("collapse_row", PayloadFree, CommandEffect::MutatesControl),
verb("expand", PayloadFree, CommandEffect::MutatesControl),
verb("expand_row", PayloadFree, CommandEffect::MutatesControl),
verb("move_focus", PayloadFree, CommandEffect::MutatesControl),
verb("move_highlight", PayloadFree, CommandEffect::MutatesControl),
verb("move_selection", PayloadFree, CommandEffect::MutatesControl),
verb("move_card", PayloadFree, CommandEffect::MutatesControl),
verb("next", PayloadFree, CommandEffect::MutatesControl),
verb("previous_page", PayloadFree, CommandEffect::MutatesControl),
verb("next_page", PayloadFree, CommandEffect::SideEffecting),
verb("select_all", PayloadFree, CommandEffect::MutatesControl),
verb("select_index", PayloadFree, CommandEffect::MutatesControl),
verb("select_row", PayloadFree, CommandEffect::MutatesControl),
verb("select_task", PayloadFree, CommandEffect::MutatesControl),
verb("clear", PayloadFree, CommandEffect::MutatesControl),
verb("toggle", PayloadFree, CommandEffect::MutatesControl),
verb("toggle_conjunction", PayloadFree, CommandEffect::MutatesControl),
verb("toggle_index", PayloadFree, CommandEffect::MutatesControl),
verb("toggle_preview_mode", PayloadFree, CommandEffect::MutatesControl),
verb("toggle_shift", PayloadFree, CommandEffect::MutatesControl),
verb("toggle_sort_column", 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("show_today", PayloadFree, CommandEffect::MutatesControl),
verb("show_with_action", PayloadFree, CommandEffect::MutatesControl),
verb("apply_preset", 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("seek_to", PayloadFree, CommandEffect::MutatesControl),
verb("backspace", PayloadFree, CommandEffect::MutatesControl),
verb("delete_char", PayloadFree, CommandEffect::MutatesControl),
verb("insert_char", PayloadFree, CommandEffect::MutatesControl),
verb("paste", PayloadFree, CommandEffect::MutatesControl),
verb("push", PayloadFree, CommandEffect::MutatesCollection),
verb("pop", PayloadFree, CommandEffect::MutatesCollection),
verb("insert", PayloadFree, CommandEffect::MutatesCollection),
verb("append_line", PayloadFree, CommandEffect::MutatesCollection),
verb("append_output", PayloadFree, CommandEffect::MutatesCollection),
verb("push_segment", PayloadFree, CommandEffect::MutatesCollection),
verb("clear_segments", PayloadFree, CommandEffect::MutatesCollection),
verb("add_item", PayloadFree, CommandEffect::MutatesCollection),
verb("remove_item", PayloadFree, CommandEffect::MutatesCollection),
verb("add_card", PayloadFree, CommandEffect::MutatesCollection),
verb("add_column", PayloadFree, CommandEffect::MutatesCollection),
verb("add_group", PayloadFree, CommandEffect::MutatesCollection),
verb("add_row", PayloadFree, CommandEffect::MutatesCollection),
verb("remove_row", PayloadFree, CommandEffect::MutatesCollection),
verb("add_tab", PayloadFree, CommandEffect::MutatesCollection),
verb("remove_tab", PayloadFree, CommandEffect::MutatesCollection),
verb("add_tag", PayloadFree, CommandEffect::MutatesCollection),
verb("remove_tag", PayloadFree, CommandEffect::MutatesCollection),
verb("add_series", PayloadFree, CommandEffect::MutatesCollection),
verb("add_action", PayloadFree, CommandEffect::MutatesCollection),
verb("add_separator", PayloadFree, CommandEffect::MutatesCollection),
verb("add_menu", PayloadFree, CommandEffect::MutatesCollection),
verb("remove_menu", PayloadFree, CommandEffect::MutatesCollection),
verb("add_property", PayloadFree, CommandEffect::MutatesCollection),
verb("add_overlay", PayloadFree, CommandEffect::MutatesCollection),
verb("add_subwindow", PayloadFree, CommandEffect::MutatesCollection),
verb("remove_subwindow", PayloadFree, CommandEffect::MutatesCollection),
verb("add_widget", PayloadFree, CommandEffect::MutatesCollection),
verb("remove_widget", PayloadFree, CommandEffect::MutatesCollection),
verb("add_large_item", 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("draw_line", PayloadFree, CommandEffect::MutatesControl),
verb("draw_rect", PayloadFree, CommandEffect::MutatesControl),
verb("fill_rect", PayloadFree, CommandEffect::MutatesControl),
verb("fill_circle", PayloadFree, CommandEffect::MutatesControl),
verb("go_back", PayloadFree, CommandEffect::SideEffecting),
verb("go_forward", PayloadFree, CommandEffect::SideEffecting),
verb("load_url", PayloadFree, CommandEffect::SideEffecting),
verb("reload", PayloadFree, CommandEffect::SideEffecting),
];
const fn verb(name: &'static str, signature: CommandSignature, effect: CommandEffect) -> VerbClass {
VerbClass { name, signature, effect }
}
struct CompositeClass {
name: &'static str,
parameter: &'static str,
parts: &'static [&'static [&'static str]],
arity: Option<usize>,
element: Option<super::types::PropertyValueKind>,
effect: CommandEffect,
}
static COMPOSITE_CLASSES: &[CompositeClass] = &[
composite_of_properties(
"set_range",
"lower",
&[&["minimum", "maximum"], &["lower", "upper"], &["min_value", "max_value"]],
Some(2),
Some(Float),
CommandEffect::MutatesControl,
),
composite("set_date_range", "range", Some(2), Some(KindString), CommandEffect::MutatesControl),
composite("set_center", "center", Some(2), Some(Float), CommandEffect::MutatesControl),
composite("set_cell", "cell", Some(3), None, CommandEffect::MutatesControl),
composite_of_properties(
"set_cursor",
"cursor",
&[&["cursor_line", "cursor_column"]],
Some(2),
Some(UInt),
CommandEffect::MutatesControl,
),
composite("set_actions", "actions", Some(2), None, CommandEffect::MutatesCollection),
composite("set_axes", "axes", Some(2), None, CommandEffect::MutatesCollection),
composite("set_candidates", "candidates", None, None, CommandEffect::MutatesCollection),
composite("set_categories", "categories", None, None, CommandEffect::MutatesCollection),
composite("set_current_font", "font", Some(2), Some(KindString), CommandEffect::MutatesControl),
composite("set_features", "features", None, Some(KindString), CommandEffect::MutatesControl),
composite("set_allowed_areas", "areas", None, Some(KindString), CommandEffect::MutatesControl),
composite("set_glyphs", "glyphs", None, None, CommandEffect::MutatesCollection),
composite("set_image", "image", Some(1), Some(KindString), CommandEffect::MutatesControl),
composite("set_model", "model", Some(1), None, CommandEffect::MutatesCollection),
composite("set_data_source", "source", Some(1), None, CommandEffect::MutatesCollection),
composite("set_data", "data", None, None, CommandEffect::MutatesCollection),
composite("set_series", "series", None, None, CommandEffect::MutatesCollection),
composite("set_tasks", "tasks", None, None, CommandEffect::MutatesCollection),
composite("set_entries", "entries", None, None, CommandEffect::MutatesCollection),
composite("set_fields", "fields", None, Some(KindString), CommandEffect::MutatesCollection),
composite("set_markers", "markers", None, None, CommandEffect::MutatesCollection),
composite("set_texts", "texts", Some(2), Some(KindString), CommandEffect::MutatesControl),
composite("set_segments", "segments", None, None, CommandEffect::MutatesCollection),
composite("set_selected_path", "path", None, None, CommandEffect::MutatesControl),
composite_of_properties(
"set_viewport",
"viewport",
&[&["viewport_start", "viewport_end"]],
Some(2),
None,
CommandEffect::MutatesControl,
),
composite_of_properties(
"set_steps",
"steps",
&[&["single_step", "page_step"]],
Some(2),
None,
CommandEffect::MutatesControl,
),
composite("set_options", "options", None, None, CommandEffect::MutatesCollection),
composite("set_items", "items", None, None, CommandEffect::MutatesCollection),
composite("set_value", "value", Some(1), None, CommandEffect::MutatesControl),
composite("set_read", "read", Some(1), Some(Bool), CommandEffect::MutatesControl),
composite("set_hex", "hex", Some(1), Some(KindString), CommandEffect::MutatesControl),
composite("set_content_widget", "widget", Some(1), None, CommandEffect::MutatesControl),
composite("set_zoom", "zoom", Some(1), Some(Float), CommandEffect::MutatesControl),
];
const fn composite(
name: &'static str,
parameter: &'static str,
arity: Option<usize>,
element: Option<super::types::PropertyValueKind>,
effect: CommandEffect,
) -> CompositeClass {
CompositeClass { name, parameter, parts: &[], arity, element, effect }
}
const fn composite_of_properties(
name: &'static str,
parameter: &'static str,
parts: &'static [&'static [&'static str]],
fallback_arity: Option<usize>,
fallback_element: Option<super::types::PropertyValueKind>,
effect: CommandEffect,
) -> CompositeClass {
CompositeClass {
name,
parameter,
parts,
arity: fallback_arity,
element: fallback_element,
effect,
}
}
fn resolved_payload(
class: &CompositeClass,
properties: &[super::types::PropertySchema],
) -> Option<(usize, Option<super::types::PropertyValueKind>)> {
for group in class.parts {
let mut kind: Option<super::types::PropertyValueKind> = None;
let mut resolved = true;
for name in *group {
let Some(schema) = properties.iter().find(|schema| schema.name == *name) else {
resolved = false;
break;
};
match kind {
Some(seen) if seen != schema.value_kind => kind = None,
_ => kind = Some(schema.value_kind),
}
}
if resolved {
return Some((group.len(), kind));
}
}
None
}
pub fn command_schema(
name: &str,
properties: &[super::types::PropertySchema],
) -> Option<CommandSchema> {
if let Some(property_name) = name.strip_prefix(SET_PREFIX) {
if let Some(schema) = properties.iter().find(|schema| schema.name == property_name) {
return Some(CommandSchema {
signature: CommandSignature::OneValue {
parameter: schema.name,
kind: schema.value_kind,
},
effect: CommandEffect::MutatesControl,
});
}
return COMPOSITE_CLASSES.iter().find(|class| class.name == name).map(|class| {
let (arity, element) = match resolved_payload(class, properties) {
Some((arity, element)) => (Some(arity), element),
None => (class.arity, class.element),
};
CommandSchema {
signature: CommandSignature::Composite {
parameter: class.parameter,
arity,
element,
},
effect: class.effect,
}
});
}
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 a_composite_setter_is_described_as_composite() {
static NO_PROPS: &[PropertySchema] = &[];
let range = command_schema("set_range", NO_PROPS).expect("set_range is a known composite");
assert!(!range.is_payload_free());
assert_eq!(range.parameter(), None, "a composite payload is not one value");
assert!(matches!(
range.signature,
CommandSignature::Composite { parameter: "lower", arity: Some(2), .. }
));
assert!(describe_command("set_range", &range).contains("set_range(lower: 2 values"));
}
#[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}");
}
}