alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Capability reference decoders.

use super::{
    DecodedGuestString, DecodedWorkspacePath, GuestDecodeError, GuestDecodeField, GuestDecodeLimits,
};
use crate::plugin::{CapabilityAtom, PluginCapabilityRef, WitCapability};

/// Capability request decoded into Alma-owned vocabulary.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DecodedCapabilityRef {
    /// `buffer.observe`.
    BufferObserve,
    /// `buffer.propose_edit`.
    BufferProposeEdit,
    /// `workspace.observe`.
    WorkspaceObserve(DecodedWorkspacePath),
    /// `workspace.artifact_write`.
    WorkspaceArtifactWrite(DecodedWorkspacePath),
    /// `status.publish`.
    StatusPublish,
}

impl DecodedCapabilityRef {
    /// Decodes a WIT capability plus optional workspace path.
    ///
    /// # Errors
    ///
    /// Returns [`GuestDecodeError`] for unknown capabilities or malformed path payloads.
    pub fn from_guest(
        capability: &[u8],
        path: Option<&[u8]>,
        limits: GuestDecodeLimits,
    ) -> Result<Self, GuestDecodeError> {
        let capability_text =
            DecodedGuestString::from_bytes(GuestDecodeField::Capability, capability, limits)?;
        let capability = WitCapability::try_from(capability_text.as_str()).map_err(|()| {
            GuestDecodeError::UnknownCapability {
                byte_len: capability_text.byte_len(),
            }
        })?;
        Self::from_atom(capability.atom(), path, limits)
    }

    /// Borrows decoded authority without widening it.
    #[must_use]
    pub const fn as_ref(&self) -> PluginCapabilityRef<'_> {
        match self {
            Self::BufferObserve => PluginCapabilityRef::BufferObserve,
            Self::BufferProposeEdit => PluginCapabilityRef::BufferProposeEdit,
            Self::WorkspaceObserve(path) => PluginCapabilityRef::WorkspaceObserve(path.as_ref()),
            Self::WorkspaceArtifactWrite(path) => {
                PluginCapabilityRef::WorkspaceArtifactWrite(path.as_ref())
            }
            Self::StatusPublish => PluginCapabilityRef::StatusPublish,
        }
    }

    /// Returns the policy atom represented by this decoded request.
    #[must_use]
    pub const fn atom(&self) -> CapabilityAtom {
        match self {
            Self::BufferObserve => CapabilityAtom::BufferObserve,
            Self::BufferProposeEdit => CapabilityAtom::BufferProposeEdit,
            Self::WorkspaceObserve(_path) => CapabilityAtom::WorkspaceObserve,
            Self::WorkspaceArtifactWrite(_path) => CapabilityAtom::WorkspaceArtifactWrite,
            Self::StatusPublish => CapabilityAtom::StatusPublish,
        }
    }

    /// Decodes an already parsed capability atom plus optional workspace path.
    fn from_atom(
        atom: CapabilityAtom,
        path: Option<&[u8]>,
        limits: GuestDecodeLimits,
    ) -> Result<Self, GuestDecodeError> {
        match atom {
            CapabilityAtom::BufferObserve => scalar_capability(atom, path, Self::BufferObserve),
            CapabilityAtom::BufferProposeEdit => {
                scalar_capability(atom, path, Self::BufferProposeEdit)
            }
            CapabilityAtom::StatusPublish => scalar_capability(atom, path, Self::StatusPublish),
            CapabilityAtom::WorkspaceObserve => Ok(Self::WorkspaceObserve(
                workspace_capability_path(atom, path, limits)?,
            )),
            CapabilityAtom::WorkspaceArtifactWrite => Ok(Self::WorkspaceArtifactWrite(
                workspace_capability_path(atom, path, limits)?,
            )),
        }
    }
}

/// Rejects path payloads on scalar capabilities.
fn scalar_capability(
    atom: CapabilityAtom,
    path: Option<&[u8]>,
    decoded: DecodedCapabilityRef,
) -> Result<DecodedCapabilityRef, GuestDecodeError> {
    if path.is_some() {
        return Err(GuestDecodeError::UnexpectedWorkspacePath {
            capability: atom.shape(),
        });
    }
    Ok(decoded)
}

/// Decodes the path required by workspace capabilities.
fn workspace_capability_path(
    atom: CapabilityAtom,
    path: Option<&[u8]>,
    limits: GuestDecodeLimits,
) -> Result<DecodedWorkspacePath, GuestDecodeError> {
    let capability = atom.shape();
    let path = path.ok_or(GuestDecodeError::MissingWorkspacePath { capability })?;
    DecodedWorkspacePath::from_bytes(capability_field(atom), path, limits)
}

/// Returns a redacted field label for capability path payloads.
const fn capability_field(atom: CapabilityAtom) -> GuestDecodeField {
    match atom {
        CapabilityAtom::BufferObserve => GuestDecodeField::BufferObservePath,
        CapabilityAtom::BufferProposeEdit => GuestDecodeField::BufferProposeEditPath,
        CapabilityAtom::WorkspaceObserve => GuestDecodeField::WorkspaceObservePath,
        CapabilityAtom::WorkspaceArtifactWrite => GuestDecodeField::WorkspaceArtifactWritePath,
        CapabilityAtom::StatusPublish => GuestDecodeField::StatusPublishPath,
    }
}