1use std::collections::BTreeMap;
13use std::sync::OnceLock;
14
15use parking_lot::RwLock;
16
17use crate::analyzer::{
18 keyword_analyzer, standard_analyzer, standard_cjk_analyzer, whitespace_analyzer, Analyzer,
19};
20use crate::error::AnalysisError;
21
22pub const DEFAULT_ANALYZER_NAME: &str = "standard";
24
25#[derive(Debug, thiserror::Error)]
26pub enum RegistryError {
27 #[error("cannot overwrite built-in analyzer: {0:?}")]
28 OverwriteBuiltin(String),
29 #[error("cannot drop built-in analyzer: {0:?}")]
30 DropBuiltin(String),
31 #[error("analyzer does not exist: {0:?}")]
32 NotFound(String),
33 #[error("analyzer {name:?} has invalid configuration: {source}")]
34 InvalidConfig {
35 name: String,
36 #[source]
37 source: AnalysisError,
38 },
39}
40
41fn builtins() -> &'static BTreeMap<&'static str, Analyzer> {
42 static B: OnceLock<BTreeMap<&'static str, Analyzer>> = OnceLock::new();
43 B.get_or_init(|| {
44 let mut m = BTreeMap::new();
45 m.insert("whitespace", whitespace_analyzer());
46 m.insert("standard", standard_analyzer("english"));
47 m.insert("standard_cjk", standard_cjk_analyzer("english"));
48 m.insert("keyword", keyword_analyzer());
49 m
50 })
51}
52
53fn custom() -> &'static RwLock<BTreeMap<String, Analyzer>> {
54 static C: OnceLock<RwLock<BTreeMap<String, Analyzer>>> = OnceLock::new();
55 C.get_or_init(|| RwLock::new(BTreeMap::new()))
56}
57
58pub fn register_analyzer(name: impl Into<String>, analyzer: Analyzer) -> Result<(), RegistryError> {
59 let name = name.into();
60 if builtins().contains_key(name.as_str()) {
61 return Err(RegistryError::OverwriteBuiltin(name));
62 }
63 analyzer
64 .validate()
65 .map_err(|source| RegistryError::InvalidConfig {
66 name: name.clone(),
67 source,
68 })?;
69 custom().write().insert(name, analyzer);
70 Ok(())
71}
72
73pub fn get_analyzer(name: &str) -> Result<Analyzer, RegistryError> {
74 if let Some(a) = custom().read().get(name) {
75 return Ok(a.clone());
76 }
77 if let Some(a) = builtins().get(name) {
78 return Ok(a.clone());
79 }
80 Err(RegistryError::NotFound(name.to_string()))
81}
82
83pub fn drop_analyzer(name: &str) -> Result<(), RegistryError> {
84 if builtins().contains_key(name) {
85 return Err(RegistryError::DropBuiltin(name.to_string()));
86 }
87 if custom().write().remove(name).is_none() {
88 return Err(RegistryError::NotFound(name.to_string()));
89 }
90 Ok(())
91}
92
93pub fn list_analyzers() -> Vec<String> {
94 let mut names: Vec<String> = builtins().keys().map(|s| (*s).to_string()).collect();
95 names.extend(custom().read().keys().cloned());
96 names.sort();
97 names.dedup();
98 names
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn builtin_names_resolvable() {
107 for name in ["whitespace", "standard", "standard_cjk", "keyword"] {
108 assert!(get_analyzer(name).is_ok(), "missing builtin: {name}");
109 }
110 }
111
112 #[test]
113 fn cannot_overwrite_builtin() {
114 let err = register_analyzer("standard", whitespace_analyzer()).unwrap_err();
115 assert!(matches!(err, RegistryError::OverwriteBuiltin(_)));
116 }
117
118 #[test]
119 fn register_get_drop_custom() {
120 register_analyzer("test_custom_alpha", whitespace_analyzer()).unwrap();
121 assert!(get_analyzer("test_custom_alpha").is_ok());
122 drop_analyzer("test_custom_alpha").unwrap();
123 assert!(matches!(
124 get_analyzer("test_custom_alpha"),
125 Err(RegistryError::NotFound(_))
126 ));
127 }
128
129 #[test]
130 fn cannot_drop_builtin() {
131 let err = drop_analyzer("standard").unwrap_err();
132 assert!(matches!(err, RegistryError::DropBuiltin(_)));
133 }
134
135 #[test]
136 fn invalid_custom_analyzer_is_not_published() {
137 let analyzer = Analyzer::new(
138 crate::Tokenizer::NGram {
139 min_gram: 0,
140 max_gram: 2,
141 },
142 Vec::new(),
143 Vec::new(),
144 );
145 let error = register_analyzer("invalid_custom", analyzer).unwrap_err();
146 assert!(matches!(error, RegistryError::InvalidConfig { .. }));
147 assert!(matches!(
148 get_analyzer("invalid_custom"),
149 Err(RegistryError::NotFound(_))
150 ));
151 }
152}