1use std::sync::Arc;
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct Symbol {
12 pub namespace: Option<Arc<str>>,
13 pub name: Arc<str>,
14 pub version: Option<Arc<str>>,
16}
17
18impl Symbol {
19 pub fn simple(name: impl Into<Arc<str>>) -> Self {
21 Self {
22 namespace: None,
23 name: name.into(),
24 version: None,
25 }
26 }
27
28 pub fn qualified(ns: impl Into<Arc<str>>, name: impl Into<Arc<str>>) -> Self {
30 Self {
31 namespace: Some(ns.into()),
32 name: name.into(),
33 version: None,
34 }
35 }
36
37 pub fn parse(s: &str) -> Self {
43 let (ns_part, name_part) = match s.find('/') {
45 Some(idx) if idx > 0 && idx < s.len() - 1 => (Some(&s[..idx]), &s[idx + 1..]),
46 _ => (None, s),
47 };
48
49 let (base_name, version) = split_version(name_part);
52
53 Symbol {
54 namespace: ns_part.map(Arc::from),
55 name: Arc::from(base_name),
56 version: version.map(Arc::from),
57 }
58 }
59
60 pub fn full_name(&self) -> String {
62 match &self.namespace {
63 Some(ns) => format!("{}/{}", ns, self.name),
64 None => self.name.to_string(),
65 }
66 }
67
68 pub fn versioned_name(&self) -> String {
71 match (&self.namespace, &self.version) {
72 (Some(ns), Some(v)) => format!("{}/{}@{}", ns, self.name, v),
73 (Some(ns), None) => format!("{}/{}", ns, self.name),
74 (None, Some(v)) => format!("{}@{}", self.name, v),
75 (None, None) => self.name.to_string(),
76 }
77 }
78}
79
80fn split_version(name_part: &str) -> (&str, Option<&str>) {
83 if let Some(at_pos) = name_part.rfind('@') {
84 let candidate = &name_part[at_pos + 1..];
85 if is_commit_hash(candidate) {
86 return (&name_part[..at_pos], Some(candidate));
87 }
88 }
89 (name_part, None)
90}
91
92pub fn is_commit_hash(s: &str) -> bool {
95 (7..=40).contains(&s.len()) && s.bytes().all(|b| b.is_ascii_hexdigit())
96}
97
98impl std::fmt::Display for Symbol {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 write!(f, "{}", self.versioned_name())
101 }
102}
103
104impl cljrs_gc::Trace for Symbol {
105 fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_simple() {
114 let s = Symbol::simple("foo");
115 assert_eq!(s.name.as_ref(), "foo");
116 assert!(s.namespace.is_none());
117 assert!(s.version.is_none());
118 assert_eq!(s.full_name(), "foo");
119 }
120
121 #[test]
122 fn test_qualified() {
123 let s = Symbol::qualified("clojure.core", "map");
124 assert_eq!(s.full_name(), "clojure.core/map");
125 assert!(s.version.is_none());
126 }
127
128 #[test]
129 fn test_parse_unversioned() {
130 assert_eq!(Symbol::parse("foo"), Symbol::simple("foo"));
131 assert_eq!(Symbol::parse("a/b"), Symbol::qualified("a", "b"));
132 assert_eq!(Symbol::parse("/").namespace, None);
133 }
134
135 #[test]
136 fn test_parse_versioned_simple() {
137 let s = Symbol::parse("my-fn@abc1234");
138 assert_eq!(s.name.as_ref(), "my-fn");
139 assert!(s.namespace.is_none());
140 assert_eq!(s.version.as_deref(), Some("abc1234"));
141 assert_eq!(s.versioned_name(), "my-fn@abc1234");
142 }
143
144 #[test]
145 fn test_parse_versioned_qualified() {
146 let s = Symbol::parse("my.ns/my-fn@abc1234");
147 assert_eq!(s.namespace.as_deref(), Some("my.ns"));
148 assert_eq!(s.name.as_ref(), "my-fn");
149 assert_eq!(s.version.as_deref(), Some("abc1234"));
150 }
151
152 #[test]
153 fn test_at_without_valid_hash_is_part_of_name() {
154 let s = Symbol::parse("my-fn@abc");
156 assert_eq!(s.name.as_ref(), "my-fn@abc");
157 assert!(s.version.is_none());
158 }
159
160 #[test]
161 fn test_at_followed_by_non_hex_is_part_of_name() {
162 let s = Symbol::parse("my-fn@not-hex");
163 assert_eq!(s.name.as_ref(), "my-fn@not-hex");
164 assert!(s.version.is_none());
165 }
166}