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`) are immutable. Users register custom
9//! analyzers under any other name; built-in names cannot be overwritten or
10//! 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
22/// Built-in default analyzer name.
23pub 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}