Skip to main content

native/guppy_nif/src/bridge_view/identity.rs

use gpui::SharedString;
use std::fmt;

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct NodeIdentity {
    resolved: String,
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct RowControlKey {
    resolved: String,
}

impl NodeIdentity {
    pub fn new(view_id: u64, path: &str, explicit_id: Option<&str>) -> Self {
        let resolved = match explicit_id {
            Some(id) => id.to_owned(),
            None => format!("guppy-{view_id}-{path}"),
        };

        Self { resolved }
    }

    pub fn to_shared_string(&self) -> SharedString {
        SharedString::from(self.resolved.clone())
    }
}

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

impl fmt::Display for NodeIdentity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.resolved)
    }
}

impl RowControlKey {
    pub fn new(view_id: u64, list_identity: &str, row_id: &str, control_id: &str) -> Self {
        Self {
            resolved: format!(
                "guppy-row-control:v1:{view_id}:{}:{}:{}",
                hex_encode_component(list_identity),
                hex_encode_component(row_id),
                hex_encode_component(control_id)
            ),
        }
    }
}

fn hex_encode_component(value: &str) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut encoded = String::with_capacity(value.len() * 2);

    for byte in value.as_bytes() {
        encoded.push(HEX[(byte >> 4) as usize] as char);
        encoded.push(HEX[(byte & 0x0f) as usize] as char);
    }

    encoded
}

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

impl fmt::Display for RowControlKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.resolved)
    }
}

#[cfg(test)]
mod tests {
    use super::{NodeIdentity, RowControlKey};

    #[test]
    fn explicit_id_wins_over_generated_path() {
        let identity = NodeIdentity::new(42, "root.1.2", Some("save_button"));
        assert_eq!(identity.to_string(), "save_button");
    }

    #[test]
    fn generated_id_uses_view_id_and_path() {
        let identity = NodeIdentity::new(42, "root.1.2", None);
        assert_eq!(identity.to_string(), "guppy-42-root.1.2");
    }

    #[test]
    fn row_control_key_uses_list_row_and_control_identity() {
        let key = RowControlKey::new(42, "todos", "row_1", "done");
        assert_eq!(
            key.to_string(),
            "guppy-row-control:v1:42:746f646f73:726f775f31:646f6e65"
        );
    }

    #[test]
    fn row_control_key_does_not_collide_on_delimiter_chars() {
        let key_a = RowControlKey::new(42, "todos:row", "item", "done");
        let key_b = RowControlKey::new(42, "todos", "row:item", "done");

        assert_ne!(key_a, key_b);
    }
}