Skip to main content

maya_mel/sema/
command_schema.rs

1//! Advanced command schema and registry contracts.
2//!
3//! Most users do not need this module directly unless they are providing their
4//! own command registry or inspecting normalized command semantics in detail.
5
6use std::{ops::Deref, sync::Arc};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum CommandKind {
10    Builtin,
11    Plugin,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum CommandSourceKind {
16    Command,
17    Script,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ReturnBehavior {
22    None,
23    Fixed(ValueShape),
24    QueryDependsOnFlag,
25    Unknown,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum FlagArity {
30    None,
31    Exact(u8),
32    Range { min: u8, max: u8 },
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct FlagArityByMode {
37    pub create: FlagArity,
38    pub edit: FlagArity,
39    pub query: FlagArity,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum ValueShape {
44    Bool,
45    Int,
46    Float,
47    String,
48    Script,
49    StringArray,
50    FloatTuple(u8),
51    IntTuple(u8),
52    NodeName,
53    Unknown,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum PositionalTailSchema {
58    None,
59    Shaped {
60        min: u8,
61        max: Option<u8>,
62        value_shapes: &'static [ValueShape],
63    },
64    Opaque {
65        min: u8,
66        max: Option<u8>,
67    },
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum PositionalSourcePolicy {
72    #[default]
73    ExplicitOnly,
74    ExplicitOrCurrentSelection,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct PositionalSlotSchema {
79    pub value_shapes: &'static [ValueShape],
80    pub source_policy: PositionalSourcePolicy,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct PositionalSchema {
85    pub prefix: &'static [PositionalSlotSchema],
86    pub tail: PositionalTailSchema,
87}
88
89impl PositionalSchema {
90    #[must_use]
91    pub const fn unconstrained() -> Self {
92        Self {
93            prefix: &[],
94            tail: PositionalTailSchema::Opaque { min: 0, max: None },
95        }
96    }
97}
98
99impl Default for PositionalSchema {
100    fn default() -> Self {
101        Self::unconstrained()
102    }
103}
104
105impl PositionalSchema {
106    fn validate(self, command_name: &Arc<str>) -> Result<(), CommandSchemaValidationError> {
107        let mut seen_selection_aware = false;
108        for (slot_index, slot) in self.prefix.iter().enumerate() {
109            let is_selection_aware = matches!(
110                slot.source_policy,
111                PositionalSourcePolicy::ExplicitOrCurrentSelection
112            );
113            if is_selection_aware {
114                seen_selection_aware = true;
115                continue;
116            }
117            if seen_selection_aware {
118                return Err(
119                    CommandSchemaValidationError::SelectionAwarePositionalNotTrailingSuffix {
120                        command_name: command_name.clone(),
121                        slot_index,
122                    },
123                );
124            }
125        }
126
127        Ok(())
128    }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct CommandModeMask {
133    pub create: bool,
134    pub edit: bool,
135    pub query: bool,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct FlagSchema {
140    pub long_name: Arc<str>,
141    pub short_name: Option<Arc<str>>,
142    pub mode_mask: CommandModeMask,
143    pub arity_by_mode: FlagArityByMode,
144    pub value_shapes: Arc<[ValueShape]>,
145    pub allows_multiple: bool,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct CommandSchema {
150    pub name: Arc<str>,
151    pub kind: CommandKind,
152    pub source_kind: CommandSourceKind,
153    pub mode_mask: CommandModeMask,
154    pub return_behavior: ReturnBehavior,
155    pub flags: Arc<[FlagSchema]>,
156    pub positionals: PositionalSchema,
157}
158
159impl CommandSchema {
160    pub fn validate(self) -> Result<ValidatedCommandSchema, CommandSchemaValidationError> {
161        self.positionals.validate(&self.name)?;
162        Ok(ValidatedCommandSchema(self))
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub enum CommandSchemaValidationError {
168    SelectionAwarePositionalNotTrailingSuffix {
169        command_name: Arc<str>,
170        slot_index: usize,
171    },
172}
173
174impl std::fmt::Display for CommandSchemaValidationError {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        match self {
177            Self::SelectionAwarePositionalNotTrailingSuffix {
178                command_name,
179                slot_index,
180            } => write!(
181                f,
182                "command schema \"{command_name}\" has non-trailing selection-aware positional slot before explicit-only slot at prefix index {slot_index}"
183            ),
184        }
185    }
186}
187
188impl std::error::Error for CommandSchemaValidationError {}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct ValidatedCommandSchema(CommandSchema);
192
193impl ValidatedCommandSchema {
194    pub fn new(schema: CommandSchema) -> Result<Self, CommandSchemaValidationError> {
195        schema.validate()
196    }
197
198    #[must_use]
199    pub fn schema(&self) -> &CommandSchema {
200        &self.0
201    }
202
203    #[must_use]
204    pub fn into_inner(self) -> CommandSchema {
205        self.0
206    }
207}
208
209impl TryFrom<CommandSchema> for ValidatedCommandSchema {
210    type Error = CommandSchemaValidationError;
211
212    fn try_from(value: CommandSchema) -> Result<Self, Self::Error> {
213        Self::new(value)
214    }
215}
216
217impl Deref for ValidatedCommandSchema {
218    type Target = CommandSchema;
219
220    fn deref(&self) -> &Self::Target {
221        self.schema()
222    }
223}
224
225pub trait CommandRegistry {
226    fn lookup(&self, name: &str) -> Option<&ValidatedCommandSchema>;
227}
228
229#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
230pub struct EmptyCommandRegistry;
231
232impl CommandRegistry for EmptyCommandRegistry {
233    fn lookup(&self, _name: &str) -> Option<&ValidatedCommandSchema> {
234        None
235    }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Default)]
239pub struct StaticCommandRegistry {
240    commands: Vec<ValidatedCommandSchema>,
241}
242
243impl StaticCommandRegistry {
244    pub fn try_new(commands: Vec<CommandSchema>) -> Result<Self, CommandSchemaValidationError> {
245        let mut validated = Vec::with_capacity(commands.len());
246        for command in commands {
247            validated.push(command.validate()?);
248        }
249        Ok(Self {
250            commands: validated,
251        })
252    }
253}
254
255impl CommandRegistry for StaticCommandRegistry {
256    fn lookup(&self, name: &str) -> Option<&ValidatedCommandSchema> {
257        self.commands.iter().find(|info| info.name.as_ref() == name)
258    }
259}