Skip to main content

uqa_analysis/
registry.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Named [`Analyzer`] registry. Generic and feature-enabled language built-ins are immutable; custom analyzers use other names.
8
9use std::collections::BTreeMap;
10use std::sync::OnceLock;
11
12use parking_lot::RwLock;
13
14use crate::analyzer::{
15    keyword_analyzer, standard_analyzer, standard_cjk_analyzer, whitespace_analyzer, Analyzer,
16};
17use crate::error::AnalysisError;
18#[cfg(feature = "nori")]
19use crate::nori::nori_analyzer;
20
21/// Built-in default analyzer name.
22pub const DEFAULT_ANALYZER_NAME: &str = "standard";
23
24#[derive(Debug, thiserror::Error)]
25pub enum RegistryError {
26    #[error("cannot overwrite built-in analyzer: {0:?}")]
27    OverwriteBuiltin(String),
28    #[error("cannot drop built-in analyzer: {0:?}")]
29    DropBuiltin(String),
30    #[error("analyzer does not exist: {0:?}")]
31    NotFound(String),
32    #[error("analyzer {name:?} has invalid configuration: {source}")]
33    InvalidConfig {
34        name: String,
35        #[source]
36        source: AnalysisError,
37    },
38}
39
40fn builtins() -> &'static BTreeMap<&'static str, Analyzer> {
41    static B: OnceLock<BTreeMap<&'static str, Analyzer>> = OnceLock::new();
42    B.get_or_init(|| {
43        let mut m = BTreeMap::new();
44        m.insert("whitespace", whitespace_analyzer());
45        m.insert("standard", standard_analyzer("english"));
46        m.insert("standard_cjk", standard_cjk_analyzer("english"));
47        m.insert("keyword", keyword_analyzer());
48        #[cfg(feature = "nori")]
49        m.insert("nori", nori_analyzer());
50        #[cfg(feature = "kuromoji")]
51        {
52            m.insert("kuromoji", crate::kuromoji::kuromoji_analyzer());
53            m.insert(
54                "kuromoji_completion",
55                crate::kuromoji::kuromoji_completion_analyzer(),
56            );
57        }
58        m
59    })
60}
61
62fn custom() -> &'static RwLock<BTreeMap<String, Analyzer>> {
63    static C: OnceLock<RwLock<BTreeMap<String, Analyzer>>> = OnceLock::new();
64    C.get_or_init(|| RwLock::new(BTreeMap::new()))
65}
66
67pub fn register_analyzer(name: impl Into<String>, analyzer: Analyzer) -> Result<(), RegistryError> {
68    let name = name.into();
69    if builtins().contains_key(name.as_str()) {
70        return Err(RegistryError::OverwriteBuiltin(name));
71    }
72    analyzer
73        .validate()
74        .map_err(|source| RegistryError::InvalidConfig {
75            name: name.clone(),
76            source,
77        })?;
78    custom().write().insert(name, analyzer);
79    Ok(())
80}
81
82/// Return whether a name is reserved by an immutable built-in analyzer.
83pub fn is_builtin_analyzer(name: &str) -> bool {
84    builtins().contains_key(name)
85}
86
87/// Return the names of every built-in analyzer exposed by this feature set.
88pub fn builtin_analyzer_names() -> Vec<String> {
89    builtins().keys().map(|name| (*name).to_owned()).collect()
90}
91
92pub fn get_analyzer(name: &str) -> Result<Analyzer, RegistryError> {
93    if let Some(a) = custom().read().get(name) {
94        return Ok(a.clone());
95    }
96    if let Some(a) = builtins().get(name) {
97        return Ok(a.clone());
98    }
99    Err(RegistryError::NotFound(name.to_string()))
100}
101
102pub fn drop_analyzer(name: &str) -> Result<(), RegistryError> {
103    if builtins().contains_key(name) {
104        return Err(RegistryError::DropBuiltin(name.to_string()));
105    }
106    if custom().write().remove(name).is_none() {
107        return Err(RegistryError::NotFound(name.to_string()));
108    }
109    Ok(())
110}
111
112pub fn list_analyzers() -> Vec<String> {
113    let mut names: Vec<String> = builtins().keys().map(|s| (*s).to_string()).collect();
114    names.extend(custom().read().keys().cloned());
115    names.sort();
116    names.dedup();
117    names
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn builtin_names_resolvable() {
126        for name in ["whitespace", "standard", "standard_cjk", "keyword"] {
127            assert!(get_analyzer(name).is_ok(), "missing builtin: {name}");
128        }
129    }
130
131    #[cfg(feature = "nori")]
132    #[test]
133    fn nori_builtin_is_resolvable_and_reserved() {
134        assert!(get_analyzer("nori").is_ok());
135        assert!(is_builtin_analyzer("nori"));
136        assert!(builtin_analyzer_names().iter().any(|name| name == "nori"));
137    }
138
139    #[test]
140    fn cannot_overwrite_builtin() {
141        let err = register_analyzer("standard", whitespace_analyzer()).unwrap_err();
142        assert!(matches!(err, RegistryError::OverwriteBuiltin(_)));
143    }
144
145    #[test]
146    fn register_get_drop_custom() {
147        register_analyzer("test_custom_alpha", whitespace_analyzer()).unwrap();
148        assert!(get_analyzer("test_custom_alpha").is_ok());
149        drop_analyzer("test_custom_alpha").unwrap();
150        assert!(matches!(
151            get_analyzer("test_custom_alpha"),
152            Err(RegistryError::NotFound(_))
153        ));
154    }
155
156    #[test]
157    fn cannot_drop_builtin() {
158        let err = drop_analyzer("standard").unwrap_err();
159        assert!(matches!(err, RegistryError::DropBuiltin(_)));
160    }
161
162    #[test]
163    fn invalid_custom_analyzer_is_not_published() {
164        let analyzer = Analyzer::new(
165            crate::Tokenizer::NGram {
166                min_gram: 0,
167                max_gram: 2,
168            },
169            Vec::new(),
170            Vec::new(),
171        );
172        let error = register_analyzer("invalid_custom", analyzer).unwrap_err();
173        assert!(matches!(error, RegistryError::InvalidConfig { .. }));
174        assert!(matches!(
175            get_analyzer("invalid_custom"),
176            Err(RegistryError::NotFound(_))
177        ));
178    }
179}