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