Skip to main content

hara_native/lang/data/
keyword.rs

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            // Java parity: Keyword.create precomputes the hash at intern time
57            // (k.hashGet() inside the RefCache factory), so first use is free.
58            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        // DEVIATION from Java: IStringType.hashCalc uses toString(), and
134        // Java's Keyword does not override toString(), so the Java hash is
135        // built on Object identity garbage ("::KEYWORD|hara.lang.data.Keyword@…")
136        // and is non-deterministic across JVM runs. This port standardises on
137        // the display form "::KEYWORD|:ns/name" (see lang::hash module docs).
138        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        // Java Keyword.create calls k.hashGet() inside the cache factory, so
249        // the cached hash is populated at intern time and first use is free.
250        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        // re-interning the same name returns the same precomputed data
261        let again = Keyword::create(Some("precomputed"), "hash").unwrap();
262        assert_eq!(again.hash_current(), current);
263        assert!(keyword.same_identity(&again));
264    }
265}