pipewire-native 0.1.4

A Rust implementation of the PipeWire client library
Documentation
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: Copyright (c) 2025 Asymptotic Inc.
// SPDX-FileCopyrightText: Copyright (c) 2025 Arun Raghavan

use std::collections::HashMap;

use pipewire_native_spa::atob;
use pipewire_native_spa::dict::Dict;
use tinyjson::{JsonParseError, JsonValue};

#[derive(Clone, Debug)]
/// A structure containing a set of properties.
pub struct Properties {
    // TODO: Make a Vec<spa::dict::ITem> here, so we can easily construct a Dict view of properties
    // at runtime, a-la pw_properties vs. spa_dict
    map: HashMap<String, String>,
}

impl Default for Properties {
    fn default() -> Self {
        Self::new()
    }
}

pub(crate) fn parse_bool(value: &str) -> bool {
    atob(value)
}

impl Properties {
    /// Create a new, empty properties structure.
    pub fn new() -> Self {
        Self {
            map: HashMap::new(),
        }
    }

    /// Create a new properties structure from a list of (key, value) tuples.
    pub fn new_vec(pairs: Vec<(String, String)>) -> Self {
        Self {
            map: HashMap::from_iter(pairs),
        }
    }

    #[allow(unused)]
    pub(crate) fn new_dict(dict: &Dict) -> Self {
        let mut map = HashMap::new();

        for (k, v) in dict.items() {
            map.insert(k.to_string(), v.to_string());
        }

        Self { map }
    }

    /// Create properties from a string representation of a JSON object.
    pub fn new_string(args: &str) -> Result<Self, String> {
        let mut p = Self::new();

        match p.update_string(args) {
            Ok(_) => Ok(p),
            Err(e) => Err(e),
        }
    }

    /// Provides an iterator of the (key, value) property pairs.
    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
        self.map.iter().map(|(k, v)| (k.as_str(), v.as_str()))
    }

    /// Update properties from given set of key/value pairs.
    pub fn merge(&mut self, new: &Properties) {
        for (k, v) in new.iter() {
            self.set(k, v.to_string());
        }
    }

    pub(crate) fn dict(&self) -> Dict {
        Dict::new(
            self.map
                .iter()
                .map(|(k, v)| (k.clone(), v.clone()))
                .collect::<Vec<(String, String)>>(),
        )
    }

    /// Sets the given `key` to the given `value`, overwriting a previous value if it exists.
    pub fn set(&mut self, key: &str, value: String) {
        self.map.insert(key.to_string(), value);
    }

    /// Unset the value for the given `key`, if it exists.
    pub fn unset(&mut self, key: &str) -> Option<String> {
        self.map.remove(key)
    }

    /// Retrieves the value for the given `key`, if it exists.
    pub fn get(&self, key: &str) -> Option<&str> {
        self.map.get(key).map(|s| s.as_str())
    }

    /// Retrieves the value for the given `key`, if it exists, and interprets it as a [`u32`].
    pub fn get_u32(&self, key: &str) -> Option<u32> {
        self.get(key).and_then(|v| v.parse::<u32>().ok())
    }

    /// Retrieves the value for the given `key`, if it exists, and interprets it as an [`i32`].
    pub fn get_i32(&self, key: &str) -> Option<i32> {
        self.get(key).and_then(|v| v.parse::<i32>().ok())
    }

    /// Retrieves the value for the given `key`, if it exists, and interprets it as a [`u64`].
    pub fn get_u64(&self, key: &str) -> Option<u64> {
        self.get(key).and_then(|v| v.parse::<u64>().ok())
    }

    /// Retrieves the value for the given `key`, if it exists, and interprets it as an [`i64`].
    pub fn get_i64(&self, key: &str) -> Option<i64> {
        self.get(key).and_then(|v| v.parse::<i64>().ok())
    }

    /// Retrieves the value for the given `key`, if it exists, and interprets it as a [`bool`].
    pub fn get_bool(&self, key: &str) -> Option<bool> {
        self.get(key).map(pipewire_native_spa::atob)
    }

    pub(crate) fn update_string(&mut self, args: &str) -> Result<u32, String> {
        let parsed: JsonValue = args.parse().map_err(|e: JsonParseError| e.to_string())?;

        if !parsed.is_object() {
            return Ok(0);
        }

        let map = match parsed {
            JsonValue::Object(m) => m,
            _ => return Ok(0),
        };

        let mut count = 0;

        for (k, v) in map {
            if v.is_null() {
                self.unset(k.as_ref());
            } else {
                let old_v = self.get(k.as_ref());
                let value = v
                    .stringify()
                    .expect("parsed value should convert back to a String");

                if old_v == Some(&value) {
                    /* Unchanged */
                    continue;
                }

                self.set(k.as_ref(), value);
                count += 1;
            }
        }

        Ok(count)
    }

    pub(crate) fn update_keys<'a>(
        &mut self,
        dict: impl Iterator<Item = (&'a str, &'a str)>,
        keys: Vec<&str>,
    ) {
        for (k, v) in dict {
            if keys.contains(&k) {
                self.set(k, v.to_string());
            }
        }
    }

    #[allow(unused)]
    pub(crate) fn update_ignore(&mut self, dict: &Dict, ignore: Vec<&str>) {
        for (k, v) in dict.items() {
            if ignore.contains(&k) {
                continue;
            }

            self.set(k, v.to_string());
        }
    }

    pub(crate) fn add_dict(&mut self, dict: &Dict) {
        for (k, v) in dict.items() {
            self.set(k, v.to_string());
        }
    }
}

#[test]
fn test_properties_simple() {
    let mut props = Properties::new();

    props.set("key1", format! {"{}", 1});

    assert_eq!(props.get("key1"), Some("1"));

    for (k, v) in props.iter() {
        assert_eq!(k, "key1");
        assert_eq!(v, "1");
    }

    assert_eq!(props.get_u32("key1"), Some(1u32));
    assert_eq!(props.get_bool("key1"), Some(true));
}

#[test]
fn test_properties_json() {
    let conf = r#"
    {
        "context.properties": {},
        "context.spa-libs": {
          "support.*": "support/libspa-support"
        },
        "context.objects": [
          {
            "factory": "spa-node-factory"
          }
        ]
    }
    "#;

    let props = Properties::new_string(conf).expect("config parsing should succeed");

    assert_eq!(props.dict().items().len(), 3);
    assert_eq!(props.get("context.properties"), Some("{}"));
    assert_eq!(
        props.get("context.objects"),
        Some(r#"[{"factory":"spa-node-factory"}]"#)
    );
}