lxmf-sdk 0.7.0

High-level Rust SDK for LXMF clients and RPC-backed LXMF workflows.
Documentation
use crate::profiles::required_capabilities;
use crate::types::{AuthMode, BindMode, OverflowPolicy, Profile, RpcBackendConfig};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::{BTreeMap, BTreeSet};

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CapabilityState {
    Enabled,
    Disabled,
    Experimental,
    Deprecated,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct CapabilityDescriptor {
    pub id: String,
    pub version: u16,
    pub state: CapabilityState,
    pub since_contract: String,
    pub deprecated_after_contract: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct EffectiveLimits {
    pub max_poll_events: usize,
    pub max_event_bytes: usize,
    pub max_batch_bytes: usize,
    pub max_extension_keys: usize,
    pub idempotency_ttl_ms: u64,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ParityReference {
    pub reticulum_conformance_ref: String,
    #[serde(default)]
    pub python_reticulum_version: Option<String>,
    pub python_reticulum_ref: String,
    #[serde(default)]
    pub python_lxmf_version: Option<String>,
    pub python_lxmf_ref: String,
}

impl Default for ParityReference {
    fn default() -> Self {
        Self {
            reticulum_conformance_ref: crate::RETICULUM_CONFORMANCE_REFERENCE_REF.to_owned(),
            python_reticulum_version: Some(crate::PYTHON_RETICULUM_REFERENCE_VERSION.to_owned()),
            python_reticulum_ref: crate::PYTHON_RETICULUM_REFERENCE_REF.to_owned(),
            python_lxmf_version: Some(crate::PYTHON_LXMF_REFERENCE_VERSION.to_owned()),
            python_lxmf_ref: crate::PYTHON_LXMF_REFERENCE_REF.to_owned(),
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct NegotiationRequest {
    pub supported_contract_versions: Vec<u16>,
    pub requested_capabilities: Vec<String>,
    pub profile: Profile,
    pub bind_mode: BindMode,
    pub auth_mode: AuthMode,
    pub overflow_policy: OverflowPolicy,
    pub block_timeout_ms: Option<u64>,
    pub rpc_backend: Option<RpcBackendConfig>,
    #[serde(default)]
    pub extensions: BTreeMap<String, JsonValue>,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct NegotiationResponse {
    pub runtime_id: String,
    pub active_contract_version: u16,
    pub effective_capabilities: Vec<String>,
    pub effective_limits: EffectiveLimits,
    pub contract_release: String,
    pub schema_namespace: String,
    #[serde(default = "crate::default_sdk_version")]
    pub sdk_version: String,
    #[serde(default)]
    pub python_reference: ParityReference,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PluginState {
    Experimental,
    Stable,
    Deprecated,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct PluginDescriptor {
    pub plugin_id: String,
    pub version: u16,
    #[serde(default)]
    pub required_capabilities: Vec<String>,
    #[serde(default)]
    pub optional_capabilities: Vec<String>,
    pub state: PluginState,
}

pub fn negotiate_contract_version(
    client_supported: &[u16],
    backend_supported: &[u16],
) -> Option<u16> {
    client_supported.iter().filter(|version| backend_supported.contains(version)).max().copied()
}

pub fn effective_capabilities_for_profile(profile: Profile) -> Vec<String> {
    required_capabilities(profile).iter().map(|capability| (*capability).to_owned()).collect()
}

pub fn negotiate_plugins(
    requested_plugin_ids: &[String],
    available_plugins: &[PluginDescriptor],
    effective_capabilities: &[String],
) -> Vec<String> {
    let capabilities = effective_capabilities
        .iter()
        .map(|capability| capability.trim().to_ascii_lowercase())
        .collect::<BTreeSet<_>>();

    let mut resolved = Vec::new();
    let mut seen = BTreeSet::new();
    for requested in requested_plugin_ids {
        let normalized_id = requested.trim().to_ascii_lowercase();
        if normalized_id.is_empty() || !seen.insert(normalized_id.clone()) {
            continue;
        }
        let Some(descriptor) = available_plugins
            .iter()
            .find(|plugin| plugin.plugin_id.trim().eq_ignore_ascii_case(normalized_id.as_str()))
        else {
            continue;
        };

        let supported = descriptor.required_capabilities.iter().all(|required_capability| {
            capabilities.contains(required_capability.trim().to_ascii_lowercase().as_str())
        });
        if supported {
            resolved.push(descriptor.plugin_id.clone());
        }
    }
    resolved
}

#[cfg(test)]
mod tests {
    use super::{
        negotiate_contract_version, negotiate_plugins, ParityReference, PluginDescriptor,
        PluginState,
    };

    #[test]
    fn negotiate_contract_version_selects_highest_overlap() {
        let selected = negotiate_contract_version(&[1, 2], &[2]);
        assert_eq!(selected, Some(2));
    }

    #[test]
    fn negotiate_contract_version_falls_back_when_future_versions_are_advertised() {
        let selected = negotiate_contract_version(&[4, 3, 2], &[2]);
        assert_eq!(selected, Some(2));
    }

    #[test]
    fn negotiate_contract_version_returns_none_without_overlap() {
        let selected = negotiate_contract_version(&[4, 3], &[2]);
        assert_eq!(selected, None);
    }

    #[test]
    fn plugin_negotiation_selects_plugins_with_supported_required_capabilities() {
        let available = vec![
            PluginDescriptor {
                plugin_id: "domain.topics".to_owned(),
                version: 1,
                required_capabilities: vec!["sdk.capability.topics".to_owned()],
                optional_capabilities: vec!["sdk.capability.topic_fanout".to_owned()],
                state: PluginState::Stable,
            },
            PluginDescriptor {
                plugin_id: "domain.voice".to_owned(),
                version: 1,
                required_capabilities: vec![
                    "sdk.capability.voice_signaling".to_owned(),
                    "sdk.capability.mtls_auth".to_owned(),
                ],
                optional_capabilities: vec![],
                state: PluginState::Experimental,
            },
        ];

        let resolved = negotiate_plugins(
            &["domain.topics".to_owned(), "domain.voice".to_owned()],
            &available,
            &["sdk.capability.topics".to_owned(), "sdk.capability.voice_signaling".to_owned()],
        );
        assert_eq!(resolved, vec!["domain.topics".to_owned()]);
    }

    #[test]
    fn plugin_negotiation_ignores_unknown_and_duplicate_plugin_requests() {
        let available = vec![PluginDescriptor {
            plugin_id: "domain.topics".to_owned(),
            version: 1,
            required_capabilities: vec!["sdk.capability.topics".to_owned()],
            optional_capabilities: vec![],
            state: PluginState::Stable,
        }];

        let resolved = negotiate_plugins(
            &["domain.unknown".to_owned(), "domain.topics".to_owned(), "DOMAIN.TOPICS".to_owned()],
            &available,
            &["sdk.capability.topics".to_owned()],
        );
        assert_eq!(resolved, vec!["domain.topics".to_owned()]);
    }

    #[test]
    fn parity_reference_preserves_unknown_versions_from_legacy_metadata() {
        let reference: ParityReference = serde_json::from_value(serde_json::json!({
            "reticulum_conformance_ref": "legacy-conformance-ref",
            "python_reticulum_ref": "legacy-reticulum-ref",
            "python_lxmf_ref": "legacy-lxmf-ref"
        }))
        .expect("legacy metadata should decode");

        assert_eq!(reference.reticulum_conformance_ref, "legacy-conformance-ref");
        assert_eq!(reference.python_reticulum_version, None);
        assert_eq!(reference.python_reticulum_ref, "legacy-reticulum-ref");
        assert_eq!(reference.python_lxmf_version, None);
        assert_eq!(reference.python_lxmf_ref, "legacy-lxmf-ref");
    }

    #[test]
    fn default_parity_reference_reports_local_reference_versions() {
        let reference = ParityReference::default();

        assert_eq!(
            reference.python_reticulum_version.as_deref(),
            Some(crate::PYTHON_RETICULUM_REFERENCE_VERSION)
        );
        assert_eq!(
            reference.python_lxmf_version.as_deref(),
            Some(crate::PYTHON_LXMF_REFERENCE_VERSION)
        );
    }
}