rs-plugin-common-interfaces 0.31.0

Common description for plugin creation
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
#[serde(transparent)]
pub struct OtherIds(pub Vec<String>);

impl OtherIds {
    fn normalize_key(key: &str) -> String {
        key.trim().to_ascii_lowercase()
    }

    fn split_entry(entry: &str) -> Option<(&str, &str)> {
        entry.split_once(':')
    }

    fn key_of(entry: &str) -> Option<String> {
        Self::split_entry(entry).map(|(key, _)| Self::normalize_key(key))
    }

    pub fn add(&mut self, key: &str, value: &str) {
        let normalized_key = Self::normalize_key(key);
        if normalized_key.is_empty() {
            return;
        }
        let entry = format!("{normalized_key}:{value}");
        if let Some(index) = self
            .0
            .iter()
            .position(|existing| Self::key_of(existing).as_deref() == Some(normalized_key.as_str()))
        {
            self.0[index] = entry;
        } else {
            self.0.push(entry);
        }
    }

    pub fn has_key(&self, key: &str) -> bool {
        let normalized_key = Self::normalize_key(key);
        if normalized_key.is_empty() {
            return false;
        }
        self.0
            .iter()
            .any(|entry| Self::key_of(entry).as_deref() == Some(normalized_key.as_str()))
    }

    pub fn get(&self, key: &str) -> Option<String> {
        let normalized_key = Self::normalize_key(key);
        if normalized_key.is_empty() {
            return None;
        }
        self.0.iter().find_map(|entry| {
            let (entry_key, entry_value) = Self::split_entry(entry)?;
            if Self::normalize_key(entry_key) == normalized_key {
                Some(entry_value.to_string())
            } else {
                None
            }
        })
    }

    pub fn contains(&self, key: &str, value: &str) -> bool {
        self.get(key).as_deref() == Some(value)
    }

    pub fn as_slice(&self) -> &[String] {
        &self.0
    }

    pub fn into_vec(self) -> Vec<String> {
        self.0
    }
}

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

impl From<OtherIds> for Vec<String> {
    fn from(value: OtherIds) -> Self {
        value.0
    }
}

#[cfg(feature = "rusqlite")]
pub mod other_ids_rusqlite {
    use rusqlite::{
        types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef},
        ToSql,
    };

    use super::OtherIds;

    impl FromSql for OtherIds {
        fn column_result(value: ValueRef) -> FromSqlResult<Self> {
            String::column_result(value).and_then(|as_string| {
                serde_json::from_str(&as_string).map_err(|_| FromSqlError::InvalidType)
            })
        }
    }
    impl ToSql for OtherIds {
        fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
            let serialized = serde_json::to_string(self).map_err(|_| FromSqlError::InvalidType)?;
            Ok(ToSqlOutput::from(serialized))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::OtherIds;

    #[test]
    fn test_add_get_contains_and_has_key() {
        let mut ids = OtherIds::default();
        ids.add("IMDB", "tt123");
        assert!(ids.has_key("imdb"));
        assert!(ids.contains("imdb", "tt123"));
        assert_eq!(ids.get("ImDb"), Some("tt123".to_string()));
    }

    #[test]
    fn test_replace_existing_key_with_latest_value() {
        let mut ids = OtherIds::default();
        ids.add("tmdb", "1");
        ids.add("TMDB", "2");
        assert_eq!(ids.as_slice(), &["tmdb:2".to_string()]);
    }

    #[test]
    fn test_preserves_values_and_key_normalization() {
        let mut ids = OtherIds::default();
        ids.add("Custom", "provider:value");
        assert_eq!(ids.as_slice(), &["custom:provider:value".to_string()]);
        assert_eq!(ids.get("custom"), Some("provider:value".to_string()));
    }

    #[cfg(feature = "rusqlite")]
    #[test]
    fn test_rusqlite_roundtrip_other_ids() -> rusqlite::Result<()> {
        use rusqlite::Connection;

        let conn = Connection::open_in_memory()?;
        conn.execute("CREATE TABLE test_other_ids (ids TEXT NOT NULL)", [])?;

        let ids = OtherIds(vec!["imdb:tt123".to_string(), "tmdb:42".to_string()]);
        conn.execute("INSERT INTO test_other_ids (ids) VALUES (?1)", [&ids])?;

        let loaded: OtherIds =
            conn.query_row("SELECT ids FROM test_other_ids LIMIT 1", [], |row| {
                row.get(0)
            })?;
        assert_eq!(loaded, ids);
        Ok(())
    }
}