Skip to main content

jpx_core/
registry.rs

1//! Function registry for runtime control and introspection.
2//!
3//! The registry provides:
4//! - Runtime enable/disable of functions (for ACLs, config-based gating)
5//! - Introspection (list available functions, their signatures, descriptions)
6//! - Category-based registration
7//! - Metadata about standard vs extension functions and JEP alignment
8
9use std::collections::{HashMap, HashSet};
10
11use crate::Runtime;
12use crate::functions::Function;
13
14/// Function category matching compile-time features.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum Category {
17    /// Standard JMESPath built-in functions (always available)
18    Standard,
19    String,
20    Array,
21    Object,
22    Math,
23    Type,
24    Utility,
25    Validation,
26    Path,
27    Expression,
28    Text,
29    Hash,
30    Encoding,
31    Regex,
32    Url,
33    Uuid,
34    Rand,
35    Datetime,
36    Fuzzy,
37    Phonetic,
38    Geo,
39    Semver,
40    Network,
41    Ids,
42    Duration,
43    Color,
44    Computing,
45    MultiMatch,
46    Jsonpatch,
47    Format,
48    Language,
49    Discovery,
50}
51
52impl Category {
53    /// Returns all categories (including Standard).
54    pub fn all() -> &'static [Category] {
55        &[
56            Category::Standard,
57            Category::String,
58            Category::Array,
59            Category::Object,
60            Category::Math,
61            Category::Type,
62            Category::Utility,
63            Category::Validation,
64            Category::Path,
65            Category::Expression,
66            Category::Text,
67            Category::Hash,
68            Category::Encoding,
69            Category::Regex,
70            Category::Url,
71            Category::Uuid,
72            Category::Rand,
73            Category::Datetime,
74            Category::Fuzzy,
75            Category::Phonetic,
76            Category::Geo,
77            Category::Semver,
78            Category::Network,
79            Category::Ids,
80            Category::Duration,
81            Category::Color,
82            Category::Computing,
83            Category::MultiMatch,
84            Category::Jsonpatch,
85            Category::Format,
86            Category::Language,
87            Category::Discovery,
88        ]
89    }
90
91    /// Returns whether this category is available (compiled in).
92    ///
93    /// In jpx-core, all categories are always available when the
94    /// `extensions` feature is enabled. Standard is always available.
95    pub fn is_available(&self) -> bool {
96        match self {
97            Category::Standard => true,
98            _ => cfg!(feature = "extensions"),
99        }
100    }
101
102    /// Returns the category name as a string.
103    pub fn name(&self) -> &'static str {
104        match self {
105            Category::Standard => "standard",
106            Category::String => "string",
107            Category::Array => "array",
108            Category::Object => "object",
109            Category::Math => "math",
110            Category::Type => "type",
111            Category::Utility => "utility",
112            Category::Validation => "validation",
113            Category::Path => "path",
114            Category::Expression => "expression",
115            Category::Text => "text",
116            Category::Hash => "hash",
117            Category::Encoding => "encoding",
118            Category::Regex => "regex",
119            Category::Url => "url",
120            Category::Uuid => "uuid",
121            Category::Rand => "rand",
122            Category::Datetime => "datetime",
123            Category::Fuzzy => "fuzzy",
124            Category::Phonetic => "phonetic",
125            Category::Geo => "geo",
126            Category::Semver => "semver",
127            Category::Network => "network",
128            Category::Ids => "ids",
129            Category::Duration => "duration",
130            Category::Color => "color",
131            Category::Computing => "computing",
132            Category::MultiMatch => "multi-match",
133            Category::Jsonpatch => "jsonpatch",
134            Category::Format => "format",
135            Category::Language => "language",
136            Category::Discovery => "discovery",
137        }
138    }
139}
140
141/// Feature tags for function classification.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
143pub enum Feature {
144    /// Standard JMESPath spec functions
145    Spec,
146    /// Core extension functions
147    Core,
148    /// Functional programming style functions
149    Fp,
150    /// JEP-aligned functions
151    Jep,
152    /// Format output functions (CSV, TSV)
153    #[allow(non_camel_case_types)]
154    format,
155    /// Environment variable access (opt-in for security)
156    #[allow(non_camel_case_types)]
157    env,
158    /// Discovery/search functions for tool discovery
159    #[allow(non_camel_case_types)]
160    discovery,
161}
162
163impl Feature {
164    /// Returns all features.
165    pub fn all() -> &'static [Feature] {
166        &[
167            Feature::Spec,
168            Feature::Core,
169            Feature::Fp,
170            Feature::Jep,
171            Feature::format,
172            Feature::env,
173            Feature::discovery,
174        ]
175    }
176
177    /// Returns the feature name as a string.
178    pub fn name(&self) -> &'static str {
179        match self {
180            Feature::Spec => "spec",
181            Feature::Core => "core",
182            Feature::Fp => "fp",
183            Feature::Jep => "jep",
184            Feature::format => "format",
185            Feature::env => "env",
186            Feature::discovery => "discovery",
187        }
188    }
189}
190
191/// Metadata about a function.
192#[derive(Debug, Clone)]
193pub struct FunctionInfo {
194    /// Function name as used in JMESPath expressions.
195    pub name: &'static str,
196    /// Category this function belongs to.
197    pub category: Category,
198    /// Human-readable description.
199    pub description: &'static str,
200    /// Argument signature (e.g., "string, string -> string").
201    pub signature: &'static str,
202    /// Example usage.
203    pub example: &'static str,
204    /// Whether this is a standard JMESPath function (vs extension).
205    pub is_standard: bool,
206    /// JMESPath Enhancement Proposal reference (e.g., "JEP-014").
207    pub jep: Option<&'static str>,
208    /// Alternative names for this function.
209    pub aliases: &'static [&'static str],
210    /// Feature tags for classification.
211    pub features: &'static [Feature],
212}
213
214/// Registry for managing function availability at runtime.
215#[derive(Debug, Clone)]
216pub struct FunctionRegistry {
217    /// Functions that have been registered.
218    registered: HashMap<&'static str, FunctionInfo>,
219    /// Functions that have been explicitly disabled.
220    disabled: HashSet<String>,
221    /// Categories that have been registered.
222    categories: HashSet<Category>,
223}
224
225impl Default for FunctionRegistry {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231impl FunctionRegistry {
232    /// Creates a new empty registry.
233    pub fn new() -> Self {
234        Self {
235            registered: HashMap::new(),
236            disabled: HashSet::new(),
237            categories: HashSet::new(),
238        }
239    }
240
241    /// Registers all available functions.
242    pub fn register_all(&mut self) -> &mut Self {
243        for category in Category::all() {
244            self.register_category(*category);
245        }
246        self
247    }
248
249    /// Registers all functions in a category.
250    pub fn register_category(&mut self, category: Category) -> &mut Self {
251        self.categories.insert(category);
252        for info in get_category_functions(category) {
253            self.registered.insert(info.name, info);
254        }
255        self
256    }
257
258    /// Disables a specific function (for ACLs).
259    pub fn disable_function(&mut self, name: &str) -> &mut Self {
260        self.disabled.insert(name.to_string());
261        self
262    }
263
264    /// Enables a previously disabled function.
265    pub fn enable_function(&mut self, name: &str) -> &mut Self {
266        self.disabled.remove(name);
267        self
268    }
269
270    /// Checks if a function is enabled.
271    pub fn is_enabled(&self, name: &str) -> bool {
272        self.registered.contains_key(name) && !self.disabled.contains(name)
273    }
274
275    /// Gets info about a specific function.
276    pub fn get_function(&self, name: &str) -> Option<&FunctionInfo> {
277        if self.disabled.contains(name) {
278            None
279        } else {
280            self.registered.get(name)
281        }
282    }
283
284    /// Iterates over all enabled functions.
285    pub fn functions(&self) -> impl Iterator<Item = &FunctionInfo> {
286        self.registered
287            .values()
288            .filter(|f| !self.disabled.contains(f.name))
289    }
290
291    /// Iterates over functions in a specific category.
292    pub fn functions_in_category(&self, category: Category) -> impl Iterator<Item = &FunctionInfo> {
293        self.registered
294            .values()
295            .filter(move |f| f.category == category && !self.disabled.contains(f.name))
296    }
297
298    /// Gets all registered categories.
299    pub fn categories(&self) -> impl Iterator<Item = &Category> {
300        self.categories.iter()
301    }
302
303    /// Gets count of enabled functions.
304    pub fn len(&self) -> usize {
305        self.registered.len() - self.disabled.len()
306    }
307
308    /// Checks if registry is empty.
309    pub fn is_empty(&self) -> bool {
310        self.len() == 0
311    }
312
313    /// Iterates over functions with a specific feature tag.
314    pub fn functions_with_feature(&self, feature: Feature) -> impl Iterator<Item = &FunctionInfo> {
315        self.registered
316            .values()
317            .filter(move |f| f.features.contains(&feature) && !self.disabled.contains(f.name))
318    }
319
320    /// Gets all spec-only (standard JMESPath) function names.
321    pub fn spec_function_names(&self) -> impl Iterator<Item = &'static str> + '_ {
322        self.functions_with_feature(Feature::Spec).map(|f| f.name)
323    }
324
325    /// Checks if a function is a standard JMESPath spec function.
326    pub fn is_spec_function(&self, name: &str) -> bool {
327        self.registered
328            .get(name)
329            .is_some_and(|f| f.features.contains(&Feature::Spec))
330    }
331
332    /// Gets function info by name or alias.
333    pub fn get_function_by_name_or_alias(&self, name: &str) -> Option<&FunctionInfo> {
334        if let Some(info) = self.get_function(name) {
335            return Some(info);
336        }
337        self.registered
338            .values()
339            .find(|f| f.aliases.contains(&name) && !self.disabled.contains(f.name))
340    }
341
342    /// Gets all aliases as (alias, canonical_name) pairs.
343    pub fn all_aliases(&self) -> impl Iterator<Item = (&'static str, &'static str)> + '_ {
344        self.registered
345            .values()
346            .flat_map(|f| f.aliases.iter().map(move |alias| (*alias, f.name)))
347    }
348
349    /// Applies the registry to a runtime, registering extension functions.
350    ///
351    /// This calls each category's `register_filtered` function to add
352    /// enabled extension functions to the runtime. Standard functions
353    /// are registered separately via `runtime.register_builtin_functions()`.
354    pub fn apply(&self, runtime: &mut Runtime) {
355        for category in &self.categories {
356            self.apply_category(runtime, *category);
357        }
358    }
359
360    #[allow(unused_variables)]
361    fn apply_category(&self, runtime: &mut Runtime, category: Category) {
362        let enabled_in_category: HashSet<&str> = self
363            .functions_in_category(category)
364            .map(|f| f.name)
365            .collect();
366
367        if enabled_in_category.is_empty() {
368            return;
369        }
370
371        // Extension function registration is gated behind the "extensions" feature.
372        // Each category module provides a register_filtered function that registers
373        // only the functions in the enabled set.
374        #[cfg(feature = "extensions")]
375        match category {
376            Category::String => {
377                crate::extensions::string::register_filtered(runtime, &enabled_in_category)
378            }
379            Category::Array => {
380                crate::extensions::array::register_filtered(runtime, &enabled_in_category)
381            }
382            Category::Object => {
383                crate::extensions::object::register_filtered(runtime, &enabled_in_category)
384            }
385            Category::Math => {
386                crate::extensions::math::register_filtered(runtime, &enabled_in_category)
387            }
388            Category::Type => {
389                crate::extensions::type_conv::register_filtered(runtime, &enabled_in_category)
390            }
391            Category::Utility => {
392                crate::extensions::utility::register_filtered(runtime, &enabled_in_category)
393            }
394            Category::Validation => {
395                crate::extensions::validation::register_filtered(runtime, &enabled_in_category)
396            }
397            Category::Path => {
398                crate::extensions::path::register_filtered(runtime, &enabled_in_category)
399            }
400            Category::Expression => {
401                crate::extensions::expression::register_filtered(runtime, &enabled_in_category)
402            }
403            Category::Text => {
404                crate::extensions::text::register_filtered(runtime, &enabled_in_category)
405            }
406            Category::Hash => {
407                crate::extensions::hash::register_filtered(runtime, &enabled_in_category)
408            }
409            Category::Encoding => {
410                crate::extensions::encoding::register_filtered(runtime, &enabled_in_category)
411            }
412            Category::Regex => {
413                crate::extensions::regex_fns::register_filtered(runtime, &enabled_in_category)
414            }
415            Category::Url => {
416                crate::extensions::url_fns::register_filtered(runtime, &enabled_in_category)
417            }
418            Category::Uuid => {
419                crate::extensions::random::register_filtered(runtime, &enabled_in_category)
420            }
421            Category::Rand => {
422                crate::extensions::random::register_filtered(runtime, &enabled_in_category)
423            }
424            Category::Datetime => {
425                crate::extensions::datetime::register_filtered(runtime, &enabled_in_category)
426            }
427            Category::Fuzzy => {
428                crate::extensions::fuzzy::register_filtered(runtime, &enabled_in_category)
429            }
430            Category::Phonetic => {
431                crate::extensions::phonetic::register_filtered(runtime, &enabled_in_category)
432            }
433            Category::Geo => {
434                crate::extensions::geo::register_filtered(runtime, &enabled_in_category)
435            }
436            Category::Semver => {
437                crate::extensions::semver_fns::register_filtered(runtime, &enabled_in_category)
438            }
439            Category::Network => {
440                crate::extensions::network::register_filtered(runtime, &enabled_in_category)
441            }
442            Category::Ids => {
443                crate::extensions::ids::register_filtered(runtime, &enabled_in_category)
444            }
445            Category::Duration => {
446                crate::extensions::duration::register_filtered(runtime, &enabled_in_category)
447            }
448            Category::Color => {
449                crate::extensions::color::register_filtered(runtime, &enabled_in_category)
450            }
451            Category::Computing => {
452                crate::extensions::computing::register_filtered(runtime, &enabled_in_category)
453            }
454            Category::MultiMatch => {
455                crate::extensions::multi_match::register_filtered(runtime, &enabled_in_category)
456            }
457            Category::Jsonpatch => {
458                crate::extensions::jsonpatch::register_filtered(runtime, &enabled_in_category)
459            }
460            Category::Format => {
461                crate::extensions::format::register_filtered(runtime, &enabled_in_category)
462            }
463            Category::Language => {
464                crate::extensions::language::register_filtered(runtime, &enabled_in_category)
465            }
466            Category::Discovery => {
467                crate::extensions::discovery::register_filtered(runtime, &enabled_in_category)
468            }
469            Category::Standard => {} // Standard functions are registered via register_builtin_functions()
470        }
471
472        // When extensions feature is not compiled, only standard functions are available.
473        #[cfg(not(feature = "extensions"))]
474        let _ = (runtime, category);
475    }
476}
477
478/// Gets function metadata for a category (from generated data).
479fn get_category_functions(category: Category) -> Vec<FunctionInfo> {
480    generated::FUNCTIONS
481        .iter()
482        .filter(|f| f.category == category)
483        .cloned()
484        .collect()
485}
486
487mod generated {
488    include!(concat!(env!("OUT_DIR"), "/registry_data.rs"));
489}
490
491/// Helper to register a function if its name is in the enabled set.
492pub fn register_if_enabled(
493    runtime: &mut Runtime,
494    name: &str,
495    enabled: &HashSet<&str>,
496    f: Box<dyn Function>,
497) {
498    if enabled.contains(name) {
499        runtime.register_function(name, f);
500    }
501}
502
503// =============================================================================
504// Synonym mapping for function discovery
505// =============================================================================
506
507/// Synonym entry mapping a search term to related function names/keywords.
508pub struct SynonymEntry {
509    /// The search term (e.g., "aggregate").
510    pub term: &'static str,
511    /// Related function names or keywords that should match.
512    pub targets: &'static [&'static str],
513}
514
515/// Gets all synonym mappings for function discovery.
516pub fn get_synonyms() -> &'static [SynonymEntry] {
517    static SYNONYMS: &[SynonymEntry] = &[
518        SynonymEntry {
519            term: "aggregate",
520            targets: &[
521                "group_by",
522                "group_by_expr",
523                "sum",
524                "avg",
525                "count",
526                "reduce",
527                "fold",
528            ],
529        },
530        SynonymEntry {
531            term: "group",
532            targets: &["group_by", "group_by_expr", "chunk", "partition"],
533        },
534        SynonymEntry {
535            term: "collect",
536            targets: &["group_by", "group_by_expr", "flatten", "merge"],
537        },
538        SynonymEntry {
539            term: "bucket",
540            targets: &["group_by", "group_by_expr", "chunk"],
541        },
542        SynonymEntry {
543            term: "count",
544            targets: &["length", "count_by", "count_if", "size"],
545        },
546        SynonymEntry {
547            term: "size",
548            targets: &["length", "count_by"],
549        },
550        SynonymEntry {
551            term: "len",
552            targets: &["length"],
553        },
554        SynonymEntry {
555            term: "concat",
556            targets: &["join", "merge", "combine"],
557        },
558        SynonymEntry {
559            term: "combine",
560            targets: &["join", "merge", "concat"],
561        },
562        SynonymEntry {
563            term: "substring",
564            targets: &["slice", "substr", "mid"],
565        },
566        SynonymEntry {
567            term: "cut",
568            targets: &["slice", "split", "trim"],
569        },
570        SynonymEntry {
571            term: "strip",
572            targets: &["trim", "trim_left", "trim_right"],
573        },
574        SynonymEntry {
575            term: "lowercase",
576            targets: &["lower", "to_lower", "downcase"],
577        },
578        SynonymEntry {
579            term: "uppercase",
580            targets: &["upper", "to_upper", "upcase"],
581        },
582        SynonymEntry {
583            term: "replace",
584            targets: &["substitute", "gsub", "regex_replace"],
585        },
586        SynonymEntry {
587            term: "find",
588            targets: &["contains", "index_of", "search", "match"],
589        },
590        SynonymEntry {
591            term: "search",
592            targets: &["contains", "find", "index_of", "regex_match"],
593        },
594        SynonymEntry {
595            term: "filter",
596            targets: &["select", "where", "find_all", "keep"],
597        },
598        SynonymEntry {
599            term: "select",
600            targets: &["filter", "map", "pluck"],
601        },
602        SynonymEntry {
603            term: "transform",
604            targets: &["map", "transform_values", "map_values"],
605        },
606        SynonymEntry {
607            term: "first",
608            targets: &["head", "take", "front"],
609        },
610        SynonymEntry {
611            term: "last",
612            targets: &["tail", "end", "back"],
613        },
614        SynonymEntry {
615            term: "remove",
616            targets: &["reject", "delete", "drop", "exclude"],
617        },
618        SynonymEntry {
619            term: "unique",
620            targets: &["distinct", "uniq", "dedupe", "deduplicate"],
621        },
622        SynonymEntry {
623            term: "dedupe",
624            targets: &["unique", "distinct", "uniq"],
625        },
626        SynonymEntry {
627            term: "shuffle",
628            targets: &["random", "randomize", "permute"],
629        },
630        SynonymEntry {
631            term: "order",
632            targets: &["sort", "sort_by", "order_by", "arrange"],
633        },
634        SynonymEntry {
635            term: "arrange",
636            targets: &["sort", "sort_by", "order_by"],
637        },
638        SynonymEntry {
639            term: "rank",
640            targets: &["sort", "sort_by", "order_by"],
641        },
642        SynonymEntry {
643            term: "average",
644            targets: &["avg", "mean", "arithmetic_mean"],
645        },
646        SynonymEntry {
647            term: "mean",
648            targets: &["avg", "average"],
649        },
650        SynonymEntry {
651            term: "total",
652            targets: &["sum", "add", "accumulate"],
653        },
654        SynonymEntry {
655            term: "add",
656            targets: &["sum", "plus", "addition"],
657        },
658        SynonymEntry {
659            term: "subtract",
660            targets: &["minus", "difference"],
661        },
662        SynonymEntry {
663            term: "multiply",
664            targets: &["times", "product", "mul"],
665        },
666        SynonymEntry {
667            term: "divide",
668            targets: &["quotient", "div"],
669        },
670        SynonymEntry {
671            term: "remainder",
672            targets: &["mod", "modulo", "modulus"],
673        },
674        SynonymEntry {
675            term: "power",
676            targets: &["pow", "exponent", "exp"],
677        },
678        SynonymEntry {
679            term: "absolute",
680            targets: &["abs", "magnitude"],
681        },
682        SynonymEntry {
683            term: "round",
684            targets: &["round", "round_to", "nearest"],
685        },
686        SynonymEntry {
687            term: "random",
688            targets: &["rand", "random_int", "random_float"],
689        },
690        SynonymEntry {
691            term: "date",
692            targets: &["now", "today", "parse_date", "format_date", "datetime"],
693        },
694        SynonymEntry {
695            term: "time",
696            targets: &["now", "time_now", "parse_time", "datetime"],
697        },
698        SynonymEntry {
699            term: "timestamp",
700            targets: &["now", "epoch", "unix_time", "to_epoch"],
701        },
702        SynonymEntry {
703            term: "format",
704            targets: &["format_date", "strftime", "date_format"],
705        },
706        SynonymEntry {
707            term: "parse",
708            targets: &["parse_date", "parse_time", "strptime", "from_string"],
709        },
710        SynonymEntry {
711            term: "convert",
712            targets: &["to_string", "to_number", "to_array", "type"],
713        },
714        SynonymEntry {
715            term: "cast",
716            targets: &["to_string", "to_number", "to_bool"],
717        },
718        SynonymEntry {
719            term: "stringify",
720            targets: &["to_string", "string", "str"],
721        },
722        SynonymEntry {
723            term: "numberify",
724            targets: &["to_number", "number", "int", "float"],
725        },
726        SynonymEntry {
727            term: "object",
728            targets: &["from_items", "to_object", "merge", "object_from_items"],
729        },
730        SynonymEntry {
731            term: "dict",
732            targets: &["from_items", "to_object", "object"],
733        },
734        SynonymEntry {
735            term: "hash",
736            targets: &["md5", "sha256", "sha1", "crc32"],
737        },
738        SynonymEntry {
739            term: "encrypt",
740            targets: &["md5", "sha256", "sha1", "hmac"],
741        },
742        SynonymEntry {
743            term: "checksum",
744            targets: &["md5", "sha256", "crc32"],
745        },
746        SynonymEntry {
747            term: "encode",
748            targets: &["base64_encode", "url_encode", "hex_encode"],
749        },
750        SynonymEntry {
751            term: "decode",
752            targets: &["base64_decode", "url_decode", "hex_decode"],
753        },
754        SynonymEntry {
755            term: "escape",
756            targets: &["url_encode", "html_escape"],
757        },
758        SynonymEntry {
759            term: "unescape",
760            targets: &["url_decode", "html_unescape"],
761        },
762        SynonymEntry {
763            term: "check",
764            targets: &[
765                "is_string",
766                "is_number",
767                "is_array",
768                "is_object",
769                "validate",
770            ],
771        },
772        SynonymEntry {
773            term: "validate",
774            targets: &["is_email", "is_url", "is_uuid", "is_valid"],
775        },
776        SynonymEntry {
777            term: "test",
778            targets: &["regex_match", "contains", "starts_with", "ends_with"],
779        },
780        SynonymEntry {
781            term: "default",
782            targets: &["coalesce", "if_null", "or_else", "default_value"],
783        },
784        SynonymEntry {
785            term: "empty",
786            targets: &["is_empty", "blank", "null"],
787        },
788        SynonymEntry {
789            term: "null",
790            targets: &["is_null", "coalesce", "not_null"],
791        },
792        SynonymEntry {
793            term: "fallback",
794            targets: &["coalesce", "default", "or_else"],
795        },
796        SynonymEntry {
797            term: "equal",
798            targets: &["eq", "equals", "same"],
799        },
800        SynonymEntry {
801            term: "compare",
802            targets: &["eq", "lt", "gt", "lte", "gte", "cmp"],
803        },
804        SynonymEntry {
805            term: "between",
806            targets: &["range", "in_range", "clamp"],
807        },
808        SynonymEntry {
809            term: "copy",
810            targets: &["clone", "dup", "duplicate"],
811        },
812        SynonymEntry {
813            term: "debug",
814            targets: &["debug", "inspect", "dump", "print"],
815        },
816        SynonymEntry {
817            term: "reverse",
818            targets: &["reverse", "flip", "invert"],
819        },
820        SynonymEntry {
821            term: "repeat",
822            targets: &["repeat", "replicate", "times"],
823        },
824        SynonymEntry {
825            term: "uuid",
826            targets: &["uuid", "uuid4", "guid", "generate_uuid"],
827        },
828        SynonymEntry {
829            term: "id",
830            targets: &["uuid", "nanoid", "ulid", "unique_id"],
831        },
832    ];
833    SYNONYMS
834}
835
836/// Looks up synonyms for a search term.
837pub fn lookup_synonyms(term: &str) -> Option<&'static [&'static str]> {
838    let term_lower = term.to_lowercase();
839    get_synonyms()
840        .iter()
841        .find(|s| s.term == term_lower)
842        .map(|s| s.targets)
843}
844
845/// Expands a search query using synonyms.
846pub fn expand_search_terms(query: &str) -> Vec<String> {
847    let mut expanded = Vec::new();
848    for word in query.split_whitespace() {
849        let word_lower = word.to_lowercase();
850        expanded.push(word_lower.clone());
851        if let Some(targets) = lookup_synonyms(&word_lower) {
852            for target in targets {
853                let target_str = (*target).to_string();
854                if !expanded.contains(&target_str) {
855                    expanded.push(target_str);
856                }
857            }
858        }
859    }
860    expanded
861}