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