lxmf-sdk 0.7.0

High-level Rust SDK for LXMF clients and RPC-backed LXMF workflows.
Documentation
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
#[non_exhaustive]
pub struct PropagationNodeSelectionState {
    #[serde(default)]
    pub peer: Option<String>,
    #[serde(default)]
    pub state: Option<String>,
    #[serde(default)]
    pub selected: bool,
    #[serde(default)]
    pub failure_kind: Option<String>,
    #[serde(default)]
    pub timed_out: bool,
    #[serde(default)]
    pub access_denied: bool,
    #[serde(default)]
    pub queue_depth: u64,
    #[serde(default)]
    pub retry_count: u64,
    #[serde(default)]
    pub next_sync_attempt: Option<i64>,
    #[serde(default)]
    pub last_sync_error: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct PropagationNodeConfig {
    #[serde(default)]
    pub enabled: bool,
    #[serde(default)]
    pub peer_announce_at_start: bool,
    #[serde(default)]
    pub peer_announce_interval_secs: Option<u64>,
    #[serde(default)]
    pub node_announce_at_start: bool,
    #[serde(default)]
    pub node_announce_interval_secs: Option<u64>,
    #[serde(default = "default_propagation_node_transfer_limit_kb")]
    pub transfer_limit_kb: u32,
    #[serde(default = "default_propagation_node_sync_limit_kb")]
    pub sync_limit_kb: u32,
    #[serde(default = "default_propagation_node_stamp_cost")]
    pub stamp_cost: u32,
    #[serde(default = "default_propagation_node_stamp_cost_flexibility")]
    pub stamp_cost_flexibility: u32,
    #[serde(default = "default_propagation_node_peering_cost")]
    pub peering_cost: u32,
    #[serde(default)]
    pub control_allowed: Vec<String>,
}

impl Default for PropagationNodeConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            peer_announce_at_start: false,
            peer_announce_interval_secs: None,
            node_announce_at_start: false,
            node_announce_interval_secs: None,
            transfer_limit_kb: default_propagation_node_transfer_limit_kb(),
            sync_limit_kb: default_propagation_node_sync_limit_kb(),
            stamp_cost: default_propagation_node_stamp_cost(),
            stamp_cost_flexibility: default_propagation_node_stamp_cost_flexibility(),
            peering_cost: default_propagation_node_peering_cost(),
            control_allowed: Vec::new(),
        }
    }
}

impl PropagationNodeConfig {
    fn from_meta(meta: &JsonValue) -> Self {
        meta.get("propagation_node")
            .cloned()
            .and_then(|value| serde_json::from_value(value).ok())
            .unwrap_or_default()
    }
}

impl PropagationNodeSelectionState {
    fn from_peer_and_meta(peer: Option<String>, meta: &JsonValue) -> Self {
        let state = propagation_node_json_string(meta, "state").ok().flatten()
            .or_else(|| propagation_node_json_string(meta, "state_name").ok().flatten());
        let failure_kind = propagation_node_json_string(meta, "failure_kind").ok().flatten();
        let timed_out = failure_kind.as_deref() == Some("timeout")
            || state.as_deref() == Some("timeout");
        let access_denied = propagation_node_json_bool(meta, "access_denied").ok().flatten().unwrap_or(false)
            || matches!(
                failure_kind.as_deref(),
                Some("access_denied" | "access-denied" | "no_access")
            );
        let selected = peer.is_some() || propagation_node_json_bool(meta, "selected").ok().flatten().unwrap_or(false);
        Self {
            peer,
            state,
            selected,
            failure_kind,
            timed_out,
            access_denied,
            queue_depth: propagation_node_json_u64(meta, "queue_depth").ok().flatten().unwrap_or(0),
            retry_count: propagation_node_json_u64(meta, "retry_count").ok().flatten().unwrap_or(0),
            next_sync_attempt: propagation_node_json_i64(meta, "next_sync_attempt").ok().flatten(),
            last_sync_error: propagation_node_json_string(meta, "last_sync_error").ok().flatten(),
        }
    }
}

#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct PropagationNodeSelectionResult {
    #[serde(default)]
    pub peer: Option<String>,
    #[serde(default)]
    pub meta: JsonValue,
    #[serde(default)]
    pub selection_state: PropagationNodeSelectionState,
    #[serde(default)]
    pub node_config: PropagationNodeConfig,
}

#[derive(Deserialize)]
struct RawPropagationNodeSelectionResult {
    #[serde(default)]
    peer: Option<String>,
    #[serde(default)]
    meta: JsonValue,
}

impl<'de> Deserialize<'de> for PropagationNodeSelectionResult {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let raw = RawPropagationNodeSelectionResult::deserialize(deserializer)?;
        let selection_state = PropagationNodeSelectionState::from_peer_and_meta(raw.peer.clone(), &raw.meta);
        let node_config = PropagationNodeConfig::from_meta(&raw.meta);
        Ok(Self {
            peer: raw.peer,
            meta: raw.meta,
            selection_state,
            node_config,
        })
    }
}

fn default_propagation_node_transfer_limit_kb() -> u32 {
    256
}

fn default_propagation_node_sync_limit_kb() -> u32 {
    10240
}

fn default_propagation_node_stamp_cost() -> u32 {
    16
}

fn default_propagation_node_stamp_cost_flexibility() -> u32 {
    3
}

fn default_propagation_node_peering_cost() -> u32 {
    18
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
#[non_exhaustive]
pub struct PropagationNodeRecord {
    #[serde(default)]
    pub peer: Option<String>,
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub last_seen: Option<i64>,
    #[serde(default)]
    pub selected: bool,
    #[serde(default)]
    pub capabilities: Vec<String>,
}

impl PropagationNodeRecord {
    fn from_node(node: &JsonValue) -> Self {
        Self {
            peer: propagation_node_json_string(node, "peer").ok().flatten(),
            name: propagation_node_json_string(node, "name").ok().flatten(),
            last_seen: propagation_node_json_i64(node, "last_seen").ok().flatten(),
            selected: propagation_node_json_bool(node, "selected").ok().flatten().unwrap_or(false),
            capabilities: propagation_node_json_string_array(node, "capabilities"),
        }
    }
}

#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct PropagationNodeListResult {
    #[serde(default)]
    pub nodes: Vec<JsonValue>,
    #[serde(default)]
    pub meta: JsonValue,
    #[serde(default)]
    pub node_records: Vec<PropagationNodeRecord>,
}

#[derive(Deserialize)]
struct RawPropagationNodeListResult {
    #[serde(default)]
    nodes: Vec<JsonValue>,
    #[serde(default)]
    meta: JsonValue,
}

impl<'de> Deserialize<'de> for PropagationNodeListResult {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let raw = RawPropagationNodeListResult::deserialize(deserializer)?;
        let node_records = raw.nodes.iter().map(PropagationNodeRecord::from_node).collect();
        Ok(Self {
            nodes: raw.nodes,
            meta: raw.meta,
            node_records,
        })
    }
}

fn propagation_node_json_bool(value: &JsonValue, key: &str) -> Result<Option<bool>, &'static str> {
    match value.get(key) {
        None => Ok(None),
        Some(v) => v.as_bool().ok_or("field is not a bool").map(Some),
    }
}

fn propagation_node_json_i64(value: &JsonValue, key: &str) -> Result<Option<i64>, &'static str> {
    match value.get(key) {
        None => Ok(None),
        Some(v) => v.as_i64().ok_or("field is not an integer").map(Some),
    }
}

fn propagation_node_json_u64(value: &JsonValue, key: &str) -> Result<Option<u64>, &'static str> {
    match value.get(key) {
        None => Ok(None),
        Some(v) => v.as_u64().ok_or("field is not an unsigned integer").map(Some),
    }
}

fn propagation_node_json_string(value: &JsonValue, key: &str) -> Result<Option<String>, &'static str> {
    match value.get(key) {
        None => Ok(None),
        Some(v) => v.as_str().ok_or("field is not a string").map(|s| Some(s.to_owned())),
    }
}

fn propagation_node_json_string_array(value: &JsonValue, key: &str) -> Vec<String> {
    value
        .get(key)
        .and_then(JsonValue::as_array)
        .map(|items| items.iter().filter_map(JsonValue::as_str).map(ToOwned::to_owned).collect())
        .unwrap_or_default()
}

#[cfg(test)]
mod propagation_node_tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn node_get_exposes_propagation_node_config_from_meta() {
        let result: PropagationNodeSelectionResult = serde_json::from_value(json!({
            "peer": null,
            "meta": {
                "propagation_node": {
                    "enabled": true,
                    "peer_announce_at_start": true,
                    "peer_announce_interval_secs": 120,
                    "node_announce_at_start": true,
                    "node_announce_interval_secs": 300,
                    "transfer_limit_kb": 512,
                    "sync_limit_kb": 20480,
                    "stamp_cost": 21,
                    "stamp_cost_flexibility": 4,
                    "peering_cost": 23,
                    "control_allowed": ["00112233445566778899aabbccddeeff"]
                }
            }
        }))
        .expect("decode node selection");

        assert!(result.node_config.enabled);
        assert!(result.node_config.peer_announce_at_start);
        assert_eq!(result.node_config.peer_announce_interval_secs, Some(120));
        assert!(result.node_config.node_announce_at_start);
        assert_eq!(result.node_config.node_announce_interval_secs, Some(300));
        assert_eq!(result.node_config.transfer_limit_kb, 512);
        assert_eq!(result.node_config.sync_limit_kb, 20480);
        assert_eq!(result.node_config.stamp_cost, 21);
        assert_eq!(result.node_config.stamp_cost_flexibility, 4);
        assert_eq!(result.node_config.peering_cost, 23);
        assert_eq!(
            result.node_config.control_allowed,
            vec!["00112233445566778899aabbccddeeff".to_string()]
        );
    }
}