lxmf-sdk 0.7.0

High-level Rust SDK for LXMF clients and RPC-backed LXMF workflows.
Documentation
use super::envelope::EnvelopeKind;

use serde::{Deserialize, Deserializer, Serialize};

use std::borrow::Borrow;

use std::collections::BTreeMap;

use std::fmt;

use std::sync::OnceLock;

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(transparent)]
pub struct OperationId(String);

type OperationIndexes = (BTreeMap<OperationId, usize>, BTreeMap<String, OperationId>);

impl OperationId {
    pub fn new(value: impl Into<String>) -> Self {
        Self(value.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl From<&str> for OperationId {
    fn from(value: &str) -> Self {
        Self::new(value)
    }
}

impl From<String> for OperationId {
    fn from(value: String) -> Self {
        Self::new(value)
    }
}

impl AsRef<str> for OperationId {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl Borrow<str> for OperationId {
    fn borrow(&self) -> &str {
        self.as_str()
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum OperationKind {
    Query,
    Command,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TransportVariant {
    App,
    Rpc,
    LegacyRpc,
    Extension,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TransportFamily {
    Local,
    Rpc,
    Legacy,
    Extension,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct OperationEntry {
    pub id: OperationId,
    pub group: String,
    pub kind: OperationKind,
    pub transport_variant: TransportVariant,
    pub description: String,
    #[serde(default)]
    pub aliases: Vec<String>,
    #[serde(default)]
    pub required_capabilities: Vec<String>,
}

impl OperationEntry {
    pub fn new(
        id: impl Into<OperationId>,
        group: impl Into<String>,
        kind: OperationKind,
        transport_variant: TransportVariant,
        description: impl Into<String>,
    ) -> Self {
        Self {
            id: id.into(),
            group: group.into(),
            kind,
            transport_variant,
            description: description.into(),
            aliases: Vec::new(),
            required_capabilities: Vec::new(),
        }
    }

    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
        self.aliases.push(alias.into());
        self
    }

    pub fn with_required_capability(mut self, capability: impl Into<String>) -> Self {
        self.required_capabilities.push(capability.into());
        self
    }

    pub fn expected_envelope_kind(&self) -> EnvelopeKind {
        match self.kind {
            OperationKind::Query => EnvelopeKind::Query,
            OperationKind::Command => EnvelopeKind::Command,
        }
    }

    pub fn accepts_envelope_kind(&self, kind: &EnvelopeKind) -> bool {
        matches!(
            (kind, &self.kind),
            (EnvelopeKind::Query, OperationKind::Query)
                | (EnvelopeKind::Command, OperationKind::Command)
        )
    }

    pub fn transport_family(&self) -> TransportFamily {
        match self.transport_variant {
            TransportVariant::App => TransportFamily::Local,
            TransportVariant::Rpc => TransportFamily::Rpc,
            TransportVariant::LegacyRpc => TransportFamily::Legacy,
            TransportVariant::Extension => TransportFamily::Extension,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedOperation<'a> {
    pub entry: &'a OperationEntry,
    pub canonical_id: &'a OperationId,
    pub alias: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RegistryError {
    DuplicateOperationId { id: OperationId },
    DuplicateAlias { alias: String, existing_id: OperationId, conflicting_id: OperationId },
    AliasConflictsWithOperationId { alias: String, operation_id: OperationId },
}

impl fmt::Display for RegistryError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DuplicateOperationId { id } => {
                write!(f, "duplicate operation id '{}'", id.as_str())
            }
            Self::DuplicateAlias { alias, existing_id, conflicting_id } => write!(
                f,
                "duplicate alias '{}' for '{}' and '{}'",
                alias,
                existing_id.as_str(),
                conflicting_id.as_str()
            ),
            Self::AliasConflictsWithOperationId { alias, operation_id } => write!(
                f,
                "alias '{}' conflicts with canonical operation id '{}'",
                alias,
                operation_id.as_str()
            ),
        }
    }
}

#[derive(Clone, Debug, Serialize, PartialEq, Eq, Default)]
pub struct OperationRegistry {
    entries: Vec<OperationEntry>,
    #[serde(skip)]
    by_id: BTreeMap<OperationId, usize>,
    #[serde(skip)]
    aliases: BTreeMap<String, OperationId>,
}

impl OperationRegistry {
    pub fn new(entries: impl IntoIterator<Item = OperationEntry>) -> Result<Self, RegistryError> {
        let entries = entries.into_iter().collect::<Vec<_>>();
        let (by_id, aliases) = Self::build_indexes(&entries)?;
        Ok(Self { entries, by_id, aliases })
    }

    pub fn built_in() -> &'static Self {
        static REGISTRY: OnceLock<OperationRegistry> = OnceLock::new();
        REGISTRY.get_or_init(|| {
            Self::new(built_in_entries()).expect("built-in app operation registry should be valid")
        })
    }

    pub fn entries(&self) -> &[OperationEntry] {
        &self.entries
    }

    pub fn get(&self, id_or_alias: impl AsRef<str>) -> Option<&OperationEntry> {
        self.resolve(id_or_alias).map(|resolved| resolved.entry)
    }

    pub fn canonicalize(&self, id_or_alias: impl AsRef<str>) -> Option<OperationId> {
        let value = id_or_alias.as_ref();
        if let Some(index) = self.by_id.get(value).copied() {
            return Some(self.entries[index].id.clone());
        }
        self.aliases.get(value).cloned()
    }

    pub fn resolve(&self, id_or_alias: impl AsRef<str>) -> Option<ResolvedOperation<'_>> {
        let value = id_or_alias.as_ref();
        let canonical = self.canonicalize(value)?;
        let index = *self.by_id.get(&canonical)?;
        Some(ResolvedOperation {
            entry: &self.entries[index],
            canonical_id: &self.entries[index].id,
            alias: (value != canonical.as_str()).then(|| value.to_owned()),
        })
    }

    pub fn entries_by_group(&self) -> BTreeMap<&str, Vec<&OperationEntry>> {
        let mut grouped = BTreeMap::<&str, Vec<&OperationEntry>>::new();
        for entry in &self.entries {
            grouped.entry(entry.group.as_str()).or_default().push(entry);
        }
        grouped
    }

    pub fn supports(&self, id_or_alias: impl AsRef<str>) -> bool {
        self.canonicalize(id_or_alias).is_some()
    }

    pub fn merged(
        &self,
        entries: impl IntoIterator<Item = OperationEntry>,
    ) -> Result<Self, RegistryError> {
        let mut merged = self.entries.clone();
        merged.extend(entries);
        Self::new(merged)
    }

    fn build_indexes(entries: &[OperationEntry]) -> Result<OperationIndexes, RegistryError> {
        let mut by_id = BTreeMap::<OperationId, usize>::new();
        let mut aliases = BTreeMap::<String, OperationId>::new();

        for (index, entry) in entries.iter().enumerate() {
            if by_id.insert(entry.id.clone(), index).is_some() {
                return Err(RegistryError::DuplicateOperationId { id: entry.id.clone() });
            }
        }

        for entry in entries {
            for alias in &entry.aliases {
                if by_id.contains_key(alias.as_str()) {
                    return Err(RegistryError::AliasConflictsWithOperationId {
                        alias: alias.clone(),
                        operation_id: OperationId::from(alias.clone()),
                    });
                }
                if let Some(existing_id) = aliases.insert(alias.clone(), entry.id.clone()) {
                    return Err(RegistryError::DuplicateAlias {
                        alias: alias.clone(),
                        existing_id,
                        conflicting_id: entry.id.clone(),
                    });
                }
            }
        }

        Ok((by_id, aliases))
    }
}

impl<'de> Deserialize<'de> for OperationRegistry {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct WireRegistry {
            entries: Vec<OperationEntry>,
        }

        let wire = WireRegistry::deserialize(deserializer)?;
        let (by_id, aliases) =
            OperationRegistry::build_indexes(&wire.entries).map_err(serde::de::Error::custom)?;
        Ok(Self { entries: wire.entries, by_id, aliases })
    }
}