ryo-symbol 0.1.0

Symbol system for Rust codebase - unique identifiers and file path management
Documentation
//! SymbolRef - Unified display format for SymbolId with SymbolPath
//!
//! Provides a consistent format: `SymbolId(2v1)@simple_jq2::Filter`
//!
//! # Usage
//!
//! ```ignore
//! let sym_ref = SymbolRef::new(id, path);
//! println!("{}", sym_ref);  // SymbolId(2v1)@simple_jq2::Filter
//!
//! // From registry lookup
//! let sym_ref = registry.get_ref(id)?;
//! ```

use std::fmt::{self, Display, Formatter};

use serde::{Deserialize, Serialize};

use crate::{SymbolId, SymbolPath};

/// Unified symbol reference with ID and Path
///
/// This is the standard format for displaying symbols with both
/// their internal ID and human-readable path.
///
/// # Format
/// ```text
/// SymbolId(2v1)@simple_jq2::Filter
/// ```
///
/// # Why this format?
/// - ID first: enables quick lookup/copy
/// - `@` separator: visually distinct, not valid in Rust paths
/// - Path second: provides context for humans
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SymbolRef {
    /// Internal symbol ID
    pub id: SymbolId,
    /// Human-readable symbol path
    pub path: SymbolPath,
}

impl SymbolRef {
    /// Create a new SymbolRef
    pub fn new(id: SymbolId, path: SymbolPath) -> Self {
        Self { id, path }
    }

    /// Get the symbol ID
    #[inline]
    pub fn id(&self) -> SymbolId {
        self.id
    }

    /// Get the symbol path
    #[inline]
    pub fn path(&self) -> &SymbolPath {
        &self.path
    }

    /// Get the symbol name (last segment of path)
    #[inline]
    pub fn name(&self) -> &str {
        self.path.name()
    }

    /// Get the crate name
    #[inline]
    pub fn crate_name(&self) -> &str {
        self.path.crate_name()
    }
}

impl Display for SymbolRef {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        // Format: SymbolId(2v1)@path::to::symbol
        write!(f, "{:?}@{}", self.id, self.path)
    }
}

/// Compact display format for SymbolRef
///
/// Use with `{:#}` formatter for compact output: `2v1@path::to::symbol`
impl SymbolRef {
    /// Format as compact string (without "SymbolId(" wrapper)
    ///
    /// Returns: `2v1@path::to::symbol`
    pub fn compact(&self) -> String {
        let id_debug = format!("{:?}", self.id);
        // Extract inner part from "SymbolId(2v1)"
        let inner = id_debug
            .strip_prefix("SymbolId(")
            .and_then(|s| s.strip_suffix(')'))
            .unwrap_or(&id_debug);
        format!("{}@{}", inner, self.path)
    }
}

// === Serialization ===

impl Serialize for SymbolRef {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // Serialize as the display format string
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for SymbolRef {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Self::parse(&s).map_err(serde::de::Error::custom)
    }
}

impl SymbolRef {
    /// Parse from string format
    ///
    /// Accepts:
    /// - `SymbolId(2v1)@path::to::symbol` (standard format)
    /// - `2v1@path::to::symbol` (compact format)
    pub fn parse(s: &str) -> Result<Self, String> {
        let (id_part, path_part) = s
            .split_once('@')
            .ok_or_else(|| format!("Invalid SymbolRef format: missing '@' separator in '{}'", s))?;

        let id =
            SymbolId::parse(id_part).ok_or_else(|| format!("Invalid SymbolId: '{}'", id_part))?;

        let path = SymbolPath::parse(path_part)
            .map_err(|e| format!("Invalid SymbolPath '{}': {:?}", path_part, e))?;

        Ok(Self { id, path })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use slotmap::SlotMap;

    fn create_test_id() -> SymbolId {
        let mut map: SlotMap<SymbolId, &str> = SlotMap::with_key();
        map.insert("test")
    }

    #[test]
    fn test_display() {
        let id = create_test_id();
        let path = SymbolPath::parse("my_crate::MyStruct").unwrap();
        let sym_ref = SymbolRef::new(id, path);

        let display = sym_ref.to_string();
        assert!(display.starts_with("SymbolId("));
        assert!(display.contains("@my_crate::MyStruct"));
    }

    #[test]
    fn test_compact() {
        let id = create_test_id();
        let path = SymbolPath::parse("my_crate::MyStruct").unwrap();
        let sym_ref = SymbolRef::new(id, path);

        let compact = sym_ref.compact();
        assert!(!compact.starts_with("SymbolId("));
        assert!(compact.contains("@my_crate::MyStruct"));
    }

    #[test]
    fn test_parse_standard() {
        let id = create_test_id();
        let path = SymbolPath::parse("my_crate::MyStruct").unwrap();
        let sym_ref = SymbolRef::new(id, path.clone());

        let display = sym_ref.to_string();
        let parsed = SymbolRef::parse(&display).unwrap();

        assert_eq!(parsed.id, id);
        assert_eq!(parsed.path, path);
    }

    #[test]
    fn test_parse_compact() {
        let id = create_test_id();
        let path = SymbolPath::parse("my_crate::MyStruct").unwrap();
        let sym_ref = SymbolRef::new(id, path.clone());

        let compact = sym_ref.compact();
        let parsed = SymbolRef::parse(&compact).unwrap();

        assert_eq!(parsed.id, id);
        assert_eq!(parsed.path, path);
    }

    #[test]
    fn test_serde_roundtrip() {
        let id = create_test_id();
        let path = SymbolPath::parse("my_crate::foo::Bar").unwrap();
        let sym_ref = SymbolRef::new(id, path);

        let json = serde_json::to_string(&sym_ref).unwrap();
        let parsed: SymbolRef = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed.id, sym_ref.id);
        assert_eq!(parsed.path, sym_ref.path);
    }

    #[test]
    fn test_accessors() {
        let id = create_test_id();
        let path = SymbolPath::parse("my_crate::foo::Bar").unwrap();
        let sym_ref = SymbolRef::new(id, path);

        assert_eq!(sym_ref.id(), id);
        assert_eq!(sym_ref.name(), "Bar");
        assert_eq!(sym_ref.crate_name(), "my_crate");
    }
}