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