Skip to main content

ryo_symbol/
symbol_ref.rs

1//! SymbolRef - Unified display format for SymbolId with SymbolPath
2//!
3//! Provides a consistent format: `SymbolId(2v1)@simple_jq2::Filter`
4//!
5//! # Usage
6//!
7//! ```ignore
8//! let sym_ref = SymbolRef::new(id, path);
9//! println!("{}", sym_ref);  // SymbolId(2v1)@simple_jq2::Filter
10//!
11//! // From registry lookup
12//! let sym_ref = registry.get_ref(id)?;
13//! ```
14
15use std::fmt::{self, Display, Formatter};
16
17use serde::{Deserialize, Serialize};
18
19use crate::{SymbolId, SymbolPath};
20
21/// Unified symbol reference with ID and Path
22///
23/// This is the standard format for displaying symbols with both
24/// their internal ID and human-readable path.
25///
26/// # Format
27/// ```text
28/// SymbolId(2v1)@simple_jq2::Filter
29/// ```
30///
31/// # Why this format?
32/// - ID first: enables quick lookup/copy
33/// - `@` separator: visually distinct, not valid in Rust paths
34/// - Path second: provides context for humans
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub struct SymbolRef {
37    /// Internal symbol ID
38    pub id: SymbolId,
39    /// Human-readable symbol path
40    pub path: SymbolPath,
41}
42
43impl SymbolRef {
44    /// Create a new SymbolRef
45    pub fn new(id: SymbolId, path: SymbolPath) -> Self {
46        Self { id, path }
47    }
48
49    /// Get the symbol ID
50    #[inline]
51    pub fn id(&self) -> SymbolId {
52        self.id
53    }
54
55    /// Get the symbol path
56    #[inline]
57    pub fn path(&self) -> &SymbolPath {
58        &self.path
59    }
60
61    /// Get the symbol name (last segment of path)
62    #[inline]
63    pub fn name(&self) -> &str {
64        self.path.name()
65    }
66
67    /// Get the crate name
68    #[inline]
69    pub fn crate_name(&self) -> &str {
70        self.path.crate_name()
71    }
72}
73
74impl Display for SymbolRef {
75    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
76        // Format: SymbolId(2v1)@path::to::symbol
77        write!(f, "{:?}@{}", self.id, self.path)
78    }
79}
80
81/// Compact display format for SymbolRef
82///
83/// Use with `{:#}` formatter for compact output: `2v1@path::to::symbol`
84impl SymbolRef {
85    /// Format as compact string (without "SymbolId(" wrapper)
86    ///
87    /// Returns: `2v1@path::to::symbol`
88    pub fn compact(&self) -> String {
89        let id_debug = format!("{:?}", self.id);
90        // Extract inner part from "SymbolId(2v1)"
91        let inner = id_debug
92            .strip_prefix("SymbolId(")
93            .and_then(|s| s.strip_suffix(')'))
94            .unwrap_or(&id_debug);
95        format!("{}@{}", inner, self.path)
96    }
97}
98
99// === Serialization ===
100
101impl Serialize for SymbolRef {
102    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103    where
104        S: serde::Serializer,
105    {
106        // Serialize as the display format string
107        serializer.serialize_str(&self.to_string())
108    }
109}
110
111impl<'de> Deserialize<'de> for SymbolRef {
112    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
113    where
114        D: serde::Deserializer<'de>,
115    {
116        let s = String::deserialize(deserializer)?;
117        Self::parse(&s).map_err(serde::de::Error::custom)
118    }
119}
120
121impl SymbolRef {
122    /// Parse from string format
123    ///
124    /// Accepts:
125    /// - `SymbolId(2v1)@path::to::symbol` (standard format)
126    /// - `2v1@path::to::symbol` (compact format)
127    pub fn parse(s: &str) -> Result<Self, String> {
128        let (id_part, path_part) = s
129            .split_once('@')
130            .ok_or_else(|| format!("Invalid SymbolRef format: missing '@' separator in '{}'", s))?;
131
132        let id =
133            SymbolId::parse(id_part).ok_or_else(|| format!("Invalid SymbolId: '{}'", id_part))?;
134
135        let path = SymbolPath::parse(path_part)
136            .map_err(|e| format!("Invalid SymbolPath '{}': {:?}", path_part, e))?;
137
138        Ok(Self { id, path })
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use slotmap::SlotMap;
146
147    fn create_test_id() -> SymbolId {
148        let mut map: SlotMap<SymbolId, &str> = SlotMap::with_key();
149        map.insert("test")
150    }
151
152    #[test]
153    fn test_display() {
154        let id = create_test_id();
155        let path = SymbolPath::parse("my_crate::MyStruct").unwrap();
156        let sym_ref = SymbolRef::new(id, path);
157
158        let display = sym_ref.to_string();
159        assert!(display.starts_with("SymbolId("));
160        assert!(display.contains("@my_crate::MyStruct"));
161    }
162
163    #[test]
164    fn test_compact() {
165        let id = create_test_id();
166        let path = SymbolPath::parse("my_crate::MyStruct").unwrap();
167        let sym_ref = SymbolRef::new(id, path);
168
169        let compact = sym_ref.compact();
170        assert!(!compact.starts_with("SymbolId("));
171        assert!(compact.contains("@my_crate::MyStruct"));
172    }
173
174    #[test]
175    fn test_parse_standard() {
176        let id = create_test_id();
177        let path = SymbolPath::parse("my_crate::MyStruct").unwrap();
178        let sym_ref = SymbolRef::new(id, path.clone());
179
180        let display = sym_ref.to_string();
181        let parsed = SymbolRef::parse(&display).unwrap();
182
183        assert_eq!(parsed.id, id);
184        assert_eq!(parsed.path, path);
185    }
186
187    #[test]
188    fn test_parse_compact() {
189        let id = create_test_id();
190        let path = SymbolPath::parse("my_crate::MyStruct").unwrap();
191        let sym_ref = SymbolRef::new(id, path.clone());
192
193        let compact = sym_ref.compact();
194        let parsed = SymbolRef::parse(&compact).unwrap();
195
196        assert_eq!(parsed.id, id);
197        assert_eq!(parsed.path, path);
198    }
199
200    #[test]
201    fn test_serde_roundtrip() {
202        let id = create_test_id();
203        let path = SymbolPath::parse("my_crate::foo::Bar").unwrap();
204        let sym_ref = SymbolRef::new(id, path);
205
206        let json = serde_json::to_string(&sym_ref).unwrap();
207        let parsed: SymbolRef = serde_json::from_str(&json).unwrap();
208
209        assert_eq!(parsed.id, sym_ref.id);
210        assert_eq!(parsed.path, sym_ref.path);
211    }
212
213    #[test]
214    fn test_accessors() {
215        let id = create_test_id();
216        let path = SymbolPath::parse("my_crate::foo::Bar").unwrap();
217        let sym_ref = SymbolRef::new(id, path);
218
219        assert_eq!(sym_ref.id(), id);
220        assert_eq!(sym_ref.name(), "Bar");
221        assert_eq!(sym_ref.crate_name(), "my_crate");
222    }
223}