maya-mel 0.1.2

Single-entry Autodesk Maya MEL parsing and analysis library.
Documentation
//! Advanced command schema and registry contracts.
//!
//! Most users do not need this module directly unless they are providing their
//! own command registry or inspecting normalized command semantics in detail.

use std::{ops::Deref, sync::Arc};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandKind {
    Builtin,
    Plugin,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandSourceKind {
    Command,
    Script,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReturnBehavior {
    None,
    Fixed(ValueShape),
    QueryDependsOnFlag,
    Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlagArity {
    None,
    Exact(u8),
    Range { min: u8, max: u8 },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FlagArityByMode {
    pub create: FlagArity,
    pub edit: FlagArity,
    pub query: FlagArity,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueShape {
    Bool,
    Int,
    Float,
    String,
    Script,
    StringArray,
    FloatTuple(u8),
    IntTuple(u8),
    NodeName,
    Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PositionalTailSchema {
    None,
    Shaped {
        min: u8,
        max: Option<u8>,
        value_shapes: &'static [ValueShape],
    },
    Opaque {
        min: u8,
        max: Option<u8>,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PositionalSourcePolicy {
    #[default]
    ExplicitOnly,
    ExplicitOrCurrentSelection,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PositionalSlotSchema {
    pub value_shapes: &'static [ValueShape],
    pub source_policy: PositionalSourcePolicy,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PositionalSchema {
    pub prefix: &'static [PositionalSlotSchema],
    pub tail: PositionalTailSchema,
}

impl PositionalSchema {
    #[must_use]
    pub const fn unconstrained() -> Self {
        Self {
            prefix: &[],
            tail: PositionalTailSchema::Opaque { min: 0, max: None },
        }
    }
}

impl Default for PositionalSchema {
    fn default() -> Self {
        Self::unconstrained()
    }
}

impl PositionalSchema {
    fn validate(self, command_name: &Arc<str>) -> Result<(), CommandSchemaValidationError> {
        let mut seen_selection_aware = false;
        for (slot_index, slot) in self.prefix.iter().enumerate() {
            let is_selection_aware = matches!(
                slot.source_policy,
                PositionalSourcePolicy::ExplicitOrCurrentSelection
            );
            if is_selection_aware {
                seen_selection_aware = true;
                continue;
            }
            if seen_selection_aware {
                return Err(
                    CommandSchemaValidationError::SelectionAwarePositionalNotTrailingSuffix {
                        command_name: command_name.clone(),
                        slot_index,
                    },
                );
            }
        }

        Ok(())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CommandModeMask {
    pub create: bool,
    pub edit: bool,
    pub query: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlagSchema {
    pub long_name: Arc<str>,
    pub short_name: Option<Arc<str>>,
    pub mode_mask: CommandModeMask,
    pub arity_by_mode: FlagArityByMode,
    pub value_shapes: Arc<[ValueShape]>,
    pub allows_multiple: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandSchema {
    pub name: Arc<str>,
    pub kind: CommandKind,
    pub source_kind: CommandSourceKind,
    pub mode_mask: CommandModeMask,
    pub return_behavior: ReturnBehavior,
    pub flags: Arc<[FlagSchema]>,
    pub positionals: PositionalSchema,
}

impl CommandSchema {
    pub fn validate(self) -> Result<ValidatedCommandSchema, CommandSchemaValidationError> {
        self.positionals.validate(&self.name)?;
        Ok(ValidatedCommandSchema(self))
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandSchemaValidationError {
    SelectionAwarePositionalNotTrailingSuffix {
        command_name: Arc<str>,
        slot_index: usize,
    },
}

impl std::fmt::Display for CommandSchemaValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::SelectionAwarePositionalNotTrailingSuffix {
                command_name,
                slot_index,
            } => write!(
                f,
                "command schema \"{command_name}\" has non-trailing selection-aware positional slot before explicit-only slot at prefix index {slot_index}"
            ),
        }
    }
}

impl std::error::Error for CommandSchemaValidationError {}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidatedCommandSchema(CommandSchema);

impl ValidatedCommandSchema {
    pub fn new(schema: CommandSchema) -> Result<Self, CommandSchemaValidationError> {
        schema.validate()
    }

    #[must_use]
    pub fn schema(&self) -> &CommandSchema {
        &self.0
    }

    #[must_use]
    pub fn into_inner(self) -> CommandSchema {
        self.0
    }
}

impl TryFrom<CommandSchema> for ValidatedCommandSchema {
    type Error = CommandSchemaValidationError;

    fn try_from(value: CommandSchema) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl Deref for ValidatedCommandSchema {
    type Target = CommandSchema;

    fn deref(&self) -> &Self::Target {
        self.schema()
    }
}

pub trait CommandRegistry {
    fn lookup(&self, name: &str) -> Option<&ValidatedCommandSchema>;
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct EmptyCommandRegistry;

impl CommandRegistry for EmptyCommandRegistry {
    fn lookup(&self, _name: &str) -> Option<&ValidatedCommandSchema> {
        None
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StaticCommandRegistry {
    commands: Vec<ValidatedCommandSchema>,
}

impl StaticCommandRegistry {
    pub fn try_new(commands: Vec<CommandSchema>) -> Result<Self, CommandSchemaValidationError> {
        let mut validated = Vec::with_capacity(commands.len());
        for command in commands {
            validated.push(command.validate()?);
        }
        Ok(Self {
            commands: validated,
        })
    }
}

impl CommandRegistry for StaticCommandRegistry {
    fn lookup(&self, name: &str) -> Option<&ValidatedCommandSchema> {
        self.commands.iter().find(|info| info.name.as_ref() == name)
    }
}