1use std::cell::{Cell, RefCell};
2use std::collections::HashMap;
3use std::rc::{Rc, Weak};
4
5use crate::lang::protocol::{
6 HashType, IDisplay, IHash, IHashCached, ILookup, IMetadata, INamespaced, IObjType, MetaType,
7 ObjType,
8};
9
10thread_local! {
11 static INTERNED: RefCell<HashMap<String, Weak<Data>>> = RefCell::new(HashMap::new());
12}
13
14#[derive(Debug)]
15struct Data {
16 namespace: Option<String>,
17 name: String,
18 full: String,
19 hash: Cell<u64>,
20}
21
22#[derive(Debug, Clone)]
23pub struct Keyword(Rc<Data>);
24
25impl Keyword {
26 pub fn create(namespace: Option<&str>, name: &str) -> Result<Self, String> {
27 let full = namespace
28 .map(|ns| format!("{ns}/{name}"))
29 .unwrap_or_else(|| name.into());
30 Ok(Self::intern(namespace, name, &full))
31 }
32
33 pub fn parse(full: &str) -> Result<Self, String> {
34 validate(full)?;
35 let slash = full.find(char::from(47));
36 Ok(Self::intern(
37 slash.map(|i| &full[..i]),
38 slash.map(|i| &full[i + 1..]).unwrap_or(full),
39 full,
40 ))
41 }
42
43 fn intern(namespace: Option<&str>, name: &str, full: &str) -> Self {
44 INTERNED.with(|cache| {
45 if let Some(value) = cache.borrow().get(full).and_then(Weak::upgrade) {
46 return Self(value);
47 }
48 let data = Rc::new(Data {
49 namespace: namespace.map(str::to_owned),
50 name: name.into(),
51 full: full.into(),
52 hash: Cell::new(0),
53 });
54 cache.borrow_mut().insert(full.into(), Rc::downgrade(&data));
55 let keyword = Self(data);
56 keyword.hash_put(keyword.hash());
59 keyword
60 })
61 }
62
63 pub fn as_str(&self) -> &str {
64 &self.0.full
65 }
66 pub fn same_identity(&self, other: &Self) -> bool {
67 Rc::ptr_eq(&self.0, &other.0)
68 }
69
70 pub fn lookup<V: Clone, L: ILookup<Self, V>>(&self, target: &L) -> Option<V> {
71 target.lookup(self)
72 }
73
74 pub fn lookup_or<V: Clone, L: ILookup<Self, V>>(&self, target: &L, fallback: V) -> V {
75 target.lookup_or(self, fallback)
76 }
77}
78
79fn validate(full: &str) -> Result<(), String> {
80 if full.is_empty() {
81 return Err("Keyword name cannot be empty.".into());
82 }
83 if full == "/" {
84 return Err("Keyword name cannot be a single slash.".into());
85 }
86 if full.bytes().filter(|byte| *byte == b'/').count() > 1 {
87 return Err("Keyword name can only contain one slash.".into());
88 }
89 if full.starts_with('/') {
90 return Err("Keyword name cannot start with a slash.".into());
91 }
92 if full.ends_with('/') {
93 return Err("Keyword name cannot end with a slash.".into());
94 }
95 Ok(())
96}
97
98impl INamespaced for Keyword {
99 fn get_name(&self) -> &str {
100 &self.0.name
101 }
102 fn get_namespace(&self) -> Option<&str> {
103 self.0.namespace.as_deref()
104 }
105}
106impl IMetadata for Keyword {
107 type Metadata = Rc<crate::lang::data::Metadata>;
108
109 fn meta(&self) -> Option<&Self::Metadata> {
110 None
111 }
112
113 fn with_meta(&self, _metadata: Option<Self::Metadata>) -> Self {
114 self.clone()
115 }
116
117 fn metatype(&self) -> MetaType {
118 MetaType::String
119 }
120}
121impl IDisplay for Keyword {
122 fn display(&self) -> String {
123 format!(":{}", self.0.full)
124 }
125}
126impl IObjType for Keyword {
127 fn obj_type(&self) -> ObjType {
128 ObjType::Keyword
129 }
130}
131impl IHash for Keyword {
132 fn hash_calc(&self, hash_type: HashType) -> u64 {
133 crate::lang::hash::hash_string_type(
139 hash_type,
140 &format!("{}|{}", self.hash_seed(), self.display()),
141 ) as u64
142 }
143 fn hash_get(&self) -> u64 {
144 self.hash_cached()
145 }
146 fn hash_get_as(&self, hash_type: HashType) -> u64 {
147 self.hash_cached_as(hash_type)
148 }
149}
150impl IHashCached for Keyword {
151 fn hash_current(&self) -> u64 {
152 self.0.hash.get()
153 }
154 fn hash_put(&self, hash: u64) {
155 self.0.hash.set(hash);
156 }
157}
158impl crate::lang::hash::JavaHash for Keyword {
159 fn java_hash(&self, hash_type: HashType) -> i64 {
160 self.hash_calc(hash_type) as i64
161 }
162}
163impl PartialEq for Keyword {
164 fn eq(&self, other: &Self) -> bool {
165 self.0.full == other.0.full
166 }
167}
168impl Eq for Keyword {}
169impl PartialOrd for Keyword {
170 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
171 Some(self.cmp(other))
172 }
173}
174impl Ord for Keyword {
175 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
176 self.0.full.cmp(&other.0.full)
177 }
178}
179impl std::hash::Hash for Keyword {
180 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
181 self.0.full.hash(state);
182 }
183}
184
185impl From<&str> for Keyword {
186 fn from(value: &str) -> Self {
187 Self::parse(value).expect("valid keyword")
188 }
189}
190impl From<String> for Keyword {
191 fn from(value: String) -> Self {
192 Self::from(value.as_str())
193 }
194}
195impl std::fmt::Display for Keyword {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 f.write_str(self.as_str())
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::Keyword;
204 use crate::lang::data::Map;
205 use crate::lang::protocol::IAssoc;
206 use crate::lang::protocol::{
207 HashType, IDisplay, IHash, IMetadata, INamespaced, IObjType, MetaType, ObjType,
208 };
209
210 #[test]
211 fn matches_java_validation_namespace_and_interning() {
212 let first = Keyword::parse("hara/name").unwrap();
213 let second = Keyword::create(Some("hara"), "name").unwrap();
214 assert!(first.same_identity(&second));
215 assert_eq!(first.get_namespace(), Some("hara"));
216 assert_eq!(first.get_name(), "name");
217 assert_eq!(first.display(), ":hara/name");
218 assert_eq!(first.obj_type(), ObjType::Keyword);
219 assert_eq!(first.hash_seed(), "::KEYWORD");
220 assert_eq!(first.metatype(), MetaType::String);
221 assert_eq!(first.hash_get(), first.hash());
222 assert_eq!(
223 first.hash_get_as(HashType::Murmur3),
224 first.hash_calc(HashType::Murmur3)
225 );
226 for invalid in ["", "/", "/name", "name/", "a/b/c"] {
227 assert!(Keyword::parse(invalid).is_err());
228 }
229
230 let values = Map::new().assoc(first.clone(), 42);
231 assert_eq!(first.lookup(&values), Some(42));
232 assert_eq!(Keyword::from("missing").lookup_or(&values, 7), 7);
233
234 let multipart = Keyword::create(Some("constructor/namespace"), "name").unwrap();
235 assert_eq!(multipart.as_str(), "constructor/namespace/name");
236 assert_eq!(multipart.get_namespace(), Some("constructor/namespace"));
237 assert_eq!(multipart.get_name(), "name");
238 assert!(Keyword::parse("constructor/namespace/name").is_err());
239
240 let documented = first.with_meta(Some(crate::lang::data::Metadata::document("ignored")));
241 assert!(documented.meta().is_none());
242 assert!(documented.same_identity(&first));
243 }
244
245 #[test]
246 fn intern_precomputes_the_hash() {
247 use crate::lang::protocol::IHashCached;
248 let keyword = Keyword::parse("precomputed/hash").unwrap();
251 let current = keyword.hash_current();
252 assert_ne!(current, 0);
253 assert_eq!(current, keyword.hash_calc(HashType::Rapid));
254 assert_eq!(keyword.hash_get(), current);
255 assert_eq!(keyword.hash_get_as(HashType::Rapid), current);
256 assert_eq!(
257 keyword.hash_get_as(HashType::Murmur3),
258 keyword.hash_calc(HashType::Murmur3)
259 );
260 let again = Keyword::create(Some("precomputed"), "hash").unwrap();
262 assert_eq!(again.hash_current(), current);
263 assert!(keyword.same_identity(&again));
264 }
265}