Skip to main content

jmespath_extensions/
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//!
9//! # Standard vs Extension Functions
10//!
11//! The registry distinguishes between:
12//! - **Standard functions** (26): Built into JMESPath spec (`abs`, `length`, `sort`, etc.)
13//! - **Extension functions** (189): Additional functions provided by this crate
14//!
15//! Standard functions are registered via `runtime.register_builtin_functions()` (from the
16//! `jmespath` crate), not via `registry.apply()`. The registry's `Category::Standard` provides
17//! metadata about these functions for introspection purposes, but does not re-register them.
18//!
19//! # JEP Alignment
20//!
21//! Some extension functions align with JMESPath Enhancement Proposals (JEPs):
22//! - **JEP-014**: String functions (`upper`, `lower`, `trim`, `split`, `replace`, etc.)
23//! - **JEP-013**: Object functions (`items`, `from_items`, `zip`)
24//!
25//! Check `FunctionInfo::jep` to see if a function aligns with a proposal.
26//!
27//! # Example
28//!
29//! ```
30//! use jmespath::Runtime;
31//! use jmespath_extensions::registry::{FunctionRegistry, Category};
32//!
33//! let mut registry = FunctionRegistry::new();
34//!
35//! // Register specific categories
36//! registry.register_category(Category::String);
37//! registry.register_category(Category::Math);
38//!
39//! // Or register all (includes Standard for introspection)
40//! // registry.register_all();
41//!
42//! // Disable specific functions for ACL
43//! registry.disable_function("md5");
44//! registry.disable_function("sha256");
45//!
46//! // Apply to runtime - registers extension functions
47//! // Note: Standard functions come from runtime.register_builtin_functions()
48//! let mut runtime = Runtime::new();
49//! runtime.register_builtin_functions(); // Standard JMESPath functions
50//! registry.apply(&mut runtime);          // Extension functions
51//!
52//! // Introspection - includes both standard and extension metadata
53//! for func in registry.functions() {
54//!     let type_label = if func.is_standard { "std" } else { "ext" };
55//!     let jep_label = func.jep.unwrap_or("-");
56//!     println!("[{}] {} ({}): {}", type_label, func.name, jep_label, func.description);
57//! }
58//! ```
59
60use jmespath::Runtime;
61use std::collections::{HashMap, HashSet};
62
63/// Function category matching compile-time features
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum Category {
66    /// Standard JMESPath built-in functions (always available)
67    Standard,
68    String,
69    Array,
70    Object,
71    Math,
72    Type,
73    Utility,
74    Validation,
75    Path,
76    Expression,
77    Text,
78    Hash,
79    Encoding,
80    Regex,
81    Url,
82    Uuid,
83    Rand,
84    Datetime,
85    Fuzzy,
86    Phonetic,
87    Geo,
88    Semver,
89    Network,
90    Ids,
91    Duration,
92    Color,
93    Computing,
94    MultiMatch,
95    Jsonpatch,
96    Format,
97    Language,
98    Discovery,
99}
100
101impl Category {
102    /// Returns all categories (including Standard)
103    pub fn all() -> &'static [Category] {
104        &[
105            Category::Standard,
106            Category::String,
107            Category::Array,
108            Category::Object,
109            Category::Math,
110            Category::Type,
111            Category::Utility,
112            Category::Validation,
113            Category::Path,
114            Category::Expression,
115            Category::Text,
116            Category::Hash,
117            Category::Encoding,
118            Category::Regex,
119            Category::Url,
120            Category::Uuid,
121            Category::Rand,
122            Category::Datetime,
123            Category::Fuzzy,
124            Category::Phonetic,
125            Category::Geo,
126            Category::Semver,
127            Category::Network,
128            Category::Ids,
129            Category::Duration,
130            Category::Color,
131            Category::Computing,
132            Category::MultiMatch,
133            Category::Jsonpatch,
134            Category::Format,
135            Category::Language,
136            Category::Discovery,
137        ]
138    }
139
140    /// Returns the category name as a string
141    pub fn name(&self) -> &'static str {
142        match self {
143            Category::Standard => "standard",
144            Category::String => "string",
145            Category::Array => "array",
146            Category::Object => "object",
147            Category::Math => "math",
148            Category::Type => "type",
149            Category::Utility => "utility",
150            Category::Validation => "validation",
151            Category::Path => "path",
152            Category::Expression => "expression",
153            Category::Text => "text",
154            Category::Hash => "hash",
155            Category::Encoding => "encoding",
156            Category::Regex => "regex",
157            Category::Url => "url",
158            Category::Uuid => "uuid",
159            Category::Rand => "rand",
160            Category::Datetime => "datetime",
161            Category::Fuzzy => "fuzzy",
162            Category::Phonetic => "phonetic",
163            Category::Geo => "geo",
164            Category::Semver => "semver",
165            Category::Network => "network",
166            Category::Ids => "ids",
167            Category::Duration => "duration",
168            Category::Color => "color",
169            Category::Computing => "computing",
170            Category::MultiMatch => "multi-match",
171            Category::Jsonpatch => "jsonpatch",
172            Category::Format => "format",
173            Category::Language => "language",
174            Category::Discovery => "discovery",
175        }
176    }
177
178    /// Check if this category is available (compiled in)
179    pub fn is_available(&self) -> bool {
180        match self {
181            // Standard functions are always available
182            Category::Standard => true,
183            #[cfg(feature = "string")]
184            Category::String => true,
185            #[cfg(feature = "array")]
186            Category::Array => true,
187            #[cfg(feature = "object")]
188            Category::Object => true,
189            #[cfg(feature = "math")]
190            Category::Math => true,
191            #[cfg(feature = "type")]
192            Category::Type => true,
193            #[cfg(feature = "utility")]
194            Category::Utility => true,
195            #[cfg(feature = "validation")]
196            Category::Validation => true,
197            #[cfg(feature = "path")]
198            Category::Path => true,
199            #[cfg(feature = "expression")]
200            Category::Expression => true,
201            #[cfg(feature = "text")]
202            Category::Text => true,
203            #[cfg(feature = "hash")]
204            Category::Hash => true,
205            #[cfg(feature = "encoding")]
206            Category::Encoding => true,
207            #[cfg(feature = "regex")]
208            Category::Regex => true,
209            #[cfg(feature = "url")]
210            Category::Url => true,
211            #[cfg(feature = "uuid")]
212            Category::Uuid => true,
213            #[cfg(feature = "rand")]
214            Category::Rand => true,
215            #[cfg(feature = "datetime")]
216            Category::Datetime => true,
217            #[cfg(feature = "fuzzy")]
218            Category::Fuzzy => true,
219            #[cfg(feature = "phonetic")]
220            Category::Phonetic => true,
221            #[cfg(feature = "geo")]
222            Category::Geo => true,
223            #[cfg(feature = "semver")]
224            Category::Semver => true,
225            #[cfg(feature = "network")]
226            Category::Network => true,
227            #[cfg(feature = "ids")]
228            Category::Ids => true,
229            #[cfg(feature = "duration")]
230            Category::Duration => true,
231            #[cfg(feature = "color")]
232            Category::Color => true,
233            #[cfg(feature = "computing")]
234            Category::Computing => true,
235            #[cfg(feature = "multi-match")]
236            Category::MultiMatch => true,
237            #[cfg(feature = "jsonpatch")]
238            Category::Jsonpatch => true,
239            #[cfg(feature = "format")]
240            Category::Format => true,
241            #[cfg(feature = "language")]
242            Category::Language => true,
243            #[cfg(feature = "discovery")]
244            Category::Discovery => true,
245            // Explicit false cases for when features are disabled.
246            // This ensures compile errors if a new category is added without handling it here.
247            #[cfg(not(feature = "string"))]
248            Category::String => false,
249            #[cfg(not(feature = "array"))]
250            Category::Array => false,
251            #[cfg(not(feature = "object"))]
252            Category::Object => false,
253            #[cfg(not(feature = "math"))]
254            Category::Math => false,
255            #[cfg(not(feature = "type"))]
256            Category::Type => false,
257            #[cfg(not(feature = "utility"))]
258            Category::Utility => false,
259            #[cfg(not(feature = "validation"))]
260            Category::Validation => false,
261            #[cfg(not(feature = "path"))]
262            Category::Path => false,
263            #[cfg(not(feature = "expression"))]
264            Category::Expression => false,
265            #[cfg(not(feature = "text"))]
266            Category::Text => false,
267            #[cfg(not(feature = "hash"))]
268            Category::Hash => false,
269            #[cfg(not(feature = "encoding"))]
270            Category::Encoding => false,
271            #[cfg(not(feature = "regex"))]
272            Category::Regex => false,
273            #[cfg(not(feature = "url"))]
274            Category::Url => false,
275            #[cfg(not(feature = "uuid"))]
276            Category::Uuid => false,
277            #[cfg(not(feature = "rand"))]
278            Category::Rand => false,
279            #[cfg(not(feature = "datetime"))]
280            Category::Datetime => false,
281            #[cfg(not(feature = "fuzzy"))]
282            Category::Fuzzy => false,
283            #[cfg(not(feature = "phonetic"))]
284            Category::Phonetic => false,
285            #[cfg(not(feature = "geo"))]
286            Category::Geo => false,
287            #[cfg(not(feature = "semver"))]
288            Category::Semver => false,
289            #[cfg(not(feature = "network"))]
290            Category::Network => false,
291            #[cfg(not(feature = "ids"))]
292            Category::Ids => false,
293            #[cfg(not(feature = "duration"))]
294            Category::Duration => false,
295            #[cfg(not(feature = "color"))]
296            Category::Color => false,
297            #[cfg(not(feature = "computing"))]
298            Category::Computing => false,
299            #[cfg(not(feature = "multi-match"))]
300            Category::MultiMatch => false,
301            #[cfg(not(feature = "jsonpatch"))]
302            Category::Jsonpatch => false,
303            #[cfg(not(feature = "format"))]
304            Category::Format => false,
305            #[cfg(not(feature = "language"))]
306            Category::Language => false,
307            #[cfg(not(feature = "discovery"))]
308            Category::Discovery => false,
309        }
310    }
311}
312
313/// Feature tags for function classification
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
315pub enum Feature {
316    /// Standard JMESPath spec functions
317    Spec,
318    /// Core jmespath-extensions functions
319    Core,
320    /// Functional programming style functions
321    Fp,
322    /// JEP-aligned functions
323    Jep,
324    /// Format output functions (CSV, TSV)
325    #[allow(non_camel_case_types)]
326    format,
327    /// Environment variable access (opt-in for security)
328    #[allow(non_camel_case_types)]
329    env,
330    /// Discovery/search functions for tool discovery
331    #[allow(non_camel_case_types)]
332    discovery,
333}
334
335impl Feature {
336    /// Returns all features
337    pub fn all() -> &'static [Feature] {
338        &[
339            Feature::Spec,
340            Feature::Core,
341            Feature::Fp,
342            Feature::Jep,
343            Feature::format,
344            Feature::env,
345            Feature::discovery,
346        ]
347    }
348
349    /// Returns the feature name as a string
350    pub fn name(&self) -> &'static str {
351        match self {
352            Feature::Spec => "spec",
353            Feature::Core => "core",
354            Feature::Fp => "fp",
355            Feature::Jep => "jep",
356            Feature::format => "format",
357            Feature::env => "env",
358            Feature::discovery => "discovery",
359        }
360    }
361}
362
363/// Metadata about a function
364#[derive(Debug, Clone)]
365pub struct FunctionInfo {
366    /// Function name as used in JMESPath expressions
367    pub name: &'static str,
368    /// Category this function belongs to
369    pub category: Category,
370    /// Human-readable description
371    pub description: &'static str,
372    /// Argument signature (e.g., "string, string -> string")
373    pub signature: &'static str,
374    /// Example usage
375    pub example: &'static str,
376    /// Whether this is a standard JMESPath function (vs extension)
377    pub is_standard: bool,
378    /// JMESPath Enhancement Proposal reference (e.g., "JEP-014")
379    /// See: <https://github.com/jmespath-community/jmespath.spec>
380    pub jep: Option<&'static str>,
381    /// Alternative names for this function (e.g., "some" for "any_expr")
382    pub aliases: &'static [&'static str],
383    /// Feature tags for classification (e.g., "fp", "core")
384    pub features: &'static [Feature],
385}
386
387/// Registry for managing function availability at runtime
388#[derive(Debug, Clone)]
389pub struct FunctionRegistry {
390    /// Functions that have been registered
391    registered: HashMap<&'static str, FunctionInfo>,
392    /// Functions that have been explicitly disabled
393    disabled: HashSet<String>,
394    /// Categories that have been registered
395    categories: HashSet<Category>,
396}
397
398impl Default for FunctionRegistry {
399    fn default() -> Self {
400        Self::new()
401    }
402}
403
404impl FunctionRegistry {
405    /// Create a new empty registry
406    pub fn new() -> Self {
407        Self {
408            registered: HashMap::new(),
409            disabled: HashSet::new(),
410            categories: HashSet::new(),
411        }
412    }
413
414    /// Register all available functions (respects compile-time features)
415    pub fn register_all(&mut self) -> &mut Self {
416        for category in Category::all() {
417            if category.is_available() {
418                self.register_category(*category);
419            }
420        }
421        self
422    }
423
424    /// Register all functions in a category
425    pub fn register_category(&mut self, category: Category) -> &mut Self {
426        if !category.is_available() {
427            return self;
428        }
429
430        self.categories.insert(category);
431
432        for info in get_category_functions(category) {
433            self.registered.insert(info.name, info);
434        }
435        self
436    }
437
438    /// Disable a specific function (for ACLs)
439    pub fn disable_function(&mut self, name: &str) -> &mut Self {
440        self.disabled.insert(name.to_string());
441        self
442    }
443
444    /// Enable a previously disabled function
445    pub fn enable_function(&mut self, name: &str) -> &mut Self {
446        self.disabled.remove(name);
447        self
448    }
449
450    /// Check if a function is enabled
451    pub fn is_enabled(&self, name: &str) -> bool {
452        self.registered.contains_key(name) && !self.disabled.contains(name)
453    }
454
455    /// Get info about a specific function
456    pub fn get_function(&self, name: &str) -> Option<&FunctionInfo> {
457        if self.disabled.contains(name) {
458            None
459        } else {
460            self.registered.get(name)
461        }
462    }
463
464    /// Iterate over all enabled functions
465    pub fn functions(&self) -> impl Iterator<Item = &FunctionInfo> {
466        self.registered
467            .values()
468            .filter(|f| !self.disabled.contains(f.name))
469    }
470
471    /// Iterate over functions in a specific category
472    pub fn functions_in_category(&self, category: Category) -> impl Iterator<Item = &FunctionInfo> {
473        self.registered
474            .values()
475            .filter(move |f| f.category == category && !self.disabled.contains(f.name))
476    }
477
478    /// Get all registered categories
479    pub fn categories(&self) -> impl Iterator<Item = &Category> {
480        self.categories.iter()
481    }
482
483    /// Get count of enabled functions
484    pub fn len(&self) -> usize {
485        self.registered.len() - self.disabled.len()
486    }
487
488    /// Check if registry is empty
489    pub fn is_empty(&self) -> bool {
490        self.len() == 0
491    }
492
493    /// Iterate over functions with a specific feature tag
494    pub fn functions_with_feature(&self, feature: Feature) -> impl Iterator<Item = &FunctionInfo> {
495        self.registered
496            .values()
497            .filter(move |f| f.features.contains(&feature) && !self.disabled.contains(f.name))
498    }
499
500    /// Get all spec-only (standard JMESPath) function names
501    pub fn spec_function_names(&self) -> impl Iterator<Item = &'static str> + '_ {
502        self.functions_with_feature(Feature::Spec).map(|f| f.name)
503    }
504
505    /// Check if a function is a standard JMESPath spec function
506    pub fn is_spec_function(&self, name: &str) -> bool {
507        self.registered
508            .get(name)
509            .map(|f| f.features.contains(&Feature::Spec))
510            .unwrap_or(false)
511    }
512
513    /// Get function info by name or alias
514    pub fn get_function_by_name_or_alias(&self, name: &str) -> Option<&FunctionInfo> {
515        // First try direct lookup
516        if let Some(info) = self.get_function(name) {
517            return Some(info);
518        }
519        // Then search aliases
520        self.registered
521            .values()
522            .find(|f| f.aliases.contains(&name) && !self.disabled.contains(f.name))
523    }
524
525    /// Get all aliases for all functions as (alias, canonical_name) pairs
526    pub fn all_aliases(&self) -> impl Iterator<Item = (&'static str, &'static str)> + '_ {
527        self.registered
528            .values()
529            .flat_map(|f| f.aliases.iter().map(move |alias| (*alias, f.name)))
530    }
531
532    /// Apply the registry to a JMESPath runtime
533    ///
534    /// This registers all enabled functions with the runtime.
535    pub fn apply(&self, runtime: &mut Runtime) {
536        for category in &self.categories {
537            if category.is_available() {
538                self.apply_category(runtime, *category);
539            }
540        }
541    }
542
543    #[allow(unused_variables)]
544    fn apply_category(&self, runtime: &mut Runtime, category: Category) {
545        // Check which functions in this category are enabled (not disabled)
546        let enabled_in_category: HashSet<&str> = self
547            .functions_in_category(category)
548            .map(|f| f.name)
549            .collect();
550
551        if enabled_in_category.is_empty() {
552            return;
553        }
554
555        // Pass the enabled set to each category's register function
556        // Only functions in this set will be registered
557        match category {
558            #[cfg(feature = "string")]
559            Category::String => crate::string::register_filtered(runtime, &enabled_in_category),
560            #[cfg(feature = "array")]
561            Category::Array => crate::array::register_filtered(runtime, &enabled_in_category),
562            #[cfg(feature = "object")]
563            Category::Object => crate::object::register_filtered(runtime, &enabled_in_category),
564            #[cfg(feature = "math")]
565            Category::Math => crate::math::register_filtered(runtime, &enabled_in_category),
566            #[cfg(feature = "type")]
567            Category::Type => crate::type_conv::register_filtered(runtime, &enabled_in_category),
568            #[cfg(feature = "utility")]
569            Category::Utility => crate::utility::register_filtered(runtime, &enabled_in_category),
570            #[cfg(feature = "validation")]
571            Category::Validation => {
572                crate::validation::register_filtered(runtime, &enabled_in_category)
573            }
574            #[cfg(feature = "path")]
575            Category::Path => crate::path::register_filtered(runtime, &enabled_in_category),
576            #[cfg(feature = "expression")]
577            Category::Expression => {
578                crate::expression::register_filtered(runtime, &enabled_in_category)
579            }
580            #[cfg(feature = "text")]
581            Category::Text => crate::text::register_filtered(runtime, &enabled_in_category),
582            #[cfg(feature = "hash")]
583            Category::Hash => crate::hash::register_filtered(runtime, &enabled_in_category),
584            #[cfg(feature = "encoding")]
585            Category::Encoding => crate::encoding::register_filtered(runtime, &enabled_in_category),
586            #[cfg(feature = "regex")]
587            Category::Regex => crate::regex_fns::register_filtered(runtime, &enabled_in_category),
588            #[cfg(feature = "url")]
589            Category::Url => crate::url_fns::register_filtered(runtime, &enabled_in_category),
590            #[cfg(feature = "uuid")]
591            Category::Uuid => crate::random::register_filtered(runtime, &enabled_in_category),
592            #[cfg(feature = "rand")]
593            Category::Rand => crate::random::register_filtered(runtime, &enabled_in_category),
594            #[cfg(feature = "datetime")]
595            Category::Datetime => crate::datetime::register_filtered(runtime, &enabled_in_category),
596            #[cfg(feature = "fuzzy")]
597            Category::Fuzzy => crate::fuzzy::register_filtered(runtime, &enabled_in_category),
598            #[cfg(feature = "phonetic")]
599            Category::Phonetic => crate::phonetic::register_filtered(runtime, &enabled_in_category),
600            #[cfg(feature = "geo")]
601            Category::Geo => crate::geo::register_filtered(runtime, &enabled_in_category),
602            #[cfg(feature = "semver")]
603            Category::Semver => crate::semver_fns::register_filtered(runtime, &enabled_in_category),
604            #[cfg(feature = "network")]
605            Category::Network => crate::network::register_filtered(runtime, &enabled_in_category),
606            #[cfg(feature = "ids")]
607            Category::Ids => crate::ids::register_filtered(runtime, &enabled_in_category),
608            #[cfg(feature = "duration")]
609            Category::Duration => crate::duration::register_filtered(runtime, &enabled_in_category),
610            #[cfg(feature = "color")]
611            Category::Color => crate::color::register_filtered(runtime, &enabled_in_category),
612            #[cfg(feature = "computing")]
613            Category::Computing => {
614                crate::computing::register_filtered(runtime, &enabled_in_category)
615            }
616            #[cfg(feature = "multi-match")]
617            Category::MultiMatch => {
618                crate::multi_match::register_filtered(runtime, &enabled_in_category)
619            }
620            #[cfg(feature = "jsonpatch")]
621            Category::Jsonpatch => {
622                crate::jsonpatch::register_filtered(runtime, &enabled_in_category)
623            }
624            #[cfg(feature = "format")]
625            Category::Format => crate::format::register_filtered(runtime, &enabled_in_category),
626            #[cfg(feature = "language")]
627            Category::Language => crate::language::register_filtered(runtime, &enabled_in_category),
628            #[cfg(feature = "discovery")]
629            Category::Discovery => {
630                crate::discovery::register_filtered(runtime, &enabled_in_category)
631            }
632            // Explicit no-op cases for when features are disabled.
633            // This ensures compile errors if a new category is added without handling it here.
634            Category::Standard => {} // Standard functions are registered via runtime.register_builtin_functions()
635            #[cfg(not(feature = "string"))]
636            Category::String => {}
637            #[cfg(not(feature = "array"))]
638            Category::Array => {}
639            #[cfg(not(feature = "object"))]
640            Category::Object => {}
641            #[cfg(not(feature = "math"))]
642            Category::Math => {}
643            #[cfg(not(feature = "type"))]
644            Category::Type => {}
645            #[cfg(not(feature = "utility"))]
646            Category::Utility => {}
647            #[cfg(not(feature = "validation"))]
648            Category::Validation => {}
649            #[cfg(not(feature = "path"))]
650            Category::Path => {}
651            #[cfg(not(feature = "expression"))]
652            Category::Expression => {}
653            #[cfg(not(feature = "text"))]
654            Category::Text => {}
655            #[cfg(not(feature = "hash"))]
656            Category::Hash => {}
657            #[cfg(not(feature = "encoding"))]
658            Category::Encoding => {}
659            #[cfg(not(feature = "regex"))]
660            Category::Regex => {}
661            #[cfg(not(feature = "url"))]
662            Category::Url => {}
663            #[cfg(not(feature = "uuid"))]
664            Category::Uuid => {}
665            #[cfg(not(feature = "rand"))]
666            Category::Rand => {}
667            #[cfg(not(feature = "datetime"))]
668            Category::Datetime => {}
669            #[cfg(not(feature = "fuzzy"))]
670            Category::Fuzzy => {}
671            #[cfg(not(feature = "phonetic"))]
672            Category::Phonetic => {}
673            #[cfg(not(feature = "geo"))]
674            Category::Geo => {}
675            #[cfg(not(feature = "semver"))]
676            Category::Semver => {}
677            #[cfg(not(feature = "network"))]
678            Category::Network => {}
679            #[cfg(not(feature = "ids"))]
680            Category::Ids => {}
681            #[cfg(not(feature = "duration"))]
682            Category::Duration => {}
683            #[cfg(not(feature = "color"))]
684            Category::Color => {}
685            #[cfg(not(feature = "computing"))]
686            Category::Computing => {}
687            #[cfg(not(feature = "multi-match"))]
688            Category::MultiMatch => {}
689            #[cfg(not(feature = "jsonpatch"))]
690            Category::Jsonpatch => {}
691            #[cfg(not(feature = "format"))]
692            Category::Format => {}
693            #[cfg(not(feature = "language"))]
694            Category::Language => {}
695            #[cfg(not(feature = "discovery"))]
696            Category::Discovery => {}
697        }
698    }
699}
700
701/// Get function metadata for a category (from generated data)
702fn get_category_functions(category: Category) -> Vec<FunctionInfo> {
703    generated::FUNCTIONS
704        .iter()
705        .filter(|f| f.category == category)
706        .cloned()
707        .collect()
708}
709
710// Include the generated function data from build.rs
711mod generated {
712    include!(concat!(env!("OUT_DIR"), "/registry_data.rs"));
713}
714
715// =============================================================================
716// Synonym mapping for function discovery
717// =============================================================================
718
719/// Synonym entry mapping a search term to related function names/keywords
720pub struct SynonymEntry {
721    /// The search term (e.g., "aggregate")
722    pub term: &'static str,
723    /// Related function names or keywords that should match
724    pub targets: &'static [&'static str],
725}
726
727/// Get all synonym mappings for function discovery
728///
729/// Maps common search terms to function names/keywords that should match.
730/// Useful for improving search when users don't know exact function names.
731///
732/// # Example
733///
734/// ```
735/// use jmespath_extensions::registry::get_synonyms;
736///
737/// let synonyms = get_synonyms();
738/// // Find what "aggregate" maps to
739/// if let Some(entry) = synonyms.iter().find(|s| s.term == "aggregate") {
740///     assert!(entry.targets.contains(&"group_by"));
741/// }
742/// ```
743pub fn get_synonyms() -> &'static [SynonymEntry] {
744    static SYNONYMS: &[SynonymEntry] = &[
745        // Aggregation/grouping synonyms
746        SynonymEntry {
747            term: "aggregate",
748            targets: &[
749                "group_by",
750                "group_by_expr",
751                "sum",
752                "avg",
753                "count",
754                "reduce",
755                "fold",
756            ],
757        },
758        SynonymEntry {
759            term: "group",
760            targets: &["group_by", "group_by_expr", "chunk", "partition"],
761        },
762        SynonymEntry {
763            term: "collect",
764            targets: &["group_by", "group_by_expr", "flatten", "merge"],
765        },
766        SynonymEntry {
767            term: "bucket",
768            targets: &["group_by", "group_by_expr", "chunk"],
769        },
770        // Counting synonyms
771        SynonymEntry {
772            term: "count",
773            targets: &["length", "count_by", "count_if", "size"],
774        },
775        SynonymEntry {
776            term: "size",
777            targets: &["length", "count_by"],
778        },
779        SynonymEntry {
780            term: "len",
781            targets: &["length"],
782        },
783        // String manipulation synonyms
784        SynonymEntry {
785            term: "concat",
786            targets: &["join", "merge", "combine"],
787        },
788        SynonymEntry {
789            term: "combine",
790            targets: &["join", "merge", "concat"],
791        },
792        SynonymEntry {
793            term: "substring",
794            targets: &["slice", "substr", "mid"],
795        },
796        SynonymEntry {
797            term: "cut",
798            targets: &["slice", "split", "trim"],
799        },
800        SynonymEntry {
801            term: "strip",
802            targets: &["trim", "trim_left", "trim_right"],
803        },
804        SynonymEntry {
805            term: "lowercase",
806            targets: &["lower", "to_lower", "downcase"],
807        },
808        SynonymEntry {
809            term: "uppercase",
810            targets: &["upper", "to_upper", "upcase"],
811        },
812        SynonymEntry {
813            term: "replace",
814            targets: &["substitute", "gsub", "regex_replace"],
815        },
816        SynonymEntry {
817            term: "find",
818            targets: &["contains", "index_of", "search", "match"],
819        },
820        SynonymEntry {
821            term: "search",
822            targets: &["contains", "find", "index_of", "regex_match"],
823        },
824        // Array/list synonyms
825        SynonymEntry {
826            term: "filter",
827            targets: &["select", "where", "find_all", "keep"],
828        },
829        SynonymEntry {
830            term: "select",
831            targets: &["filter", "map", "pluck"],
832        },
833        SynonymEntry {
834            term: "transform",
835            targets: &["map", "transform_values", "map_values"],
836        },
837        SynonymEntry {
838            term: "first",
839            targets: &["head", "take", "front"],
840        },
841        SynonymEntry {
842            term: "last",
843            targets: &["tail", "end", "back"],
844        },
845        SynonymEntry {
846            term: "remove",
847            targets: &["reject", "delete", "drop", "exclude"],
848        },
849        SynonymEntry {
850            term: "unique",
851            targets: &["distinct", "uniq", "dedupe", "deduplicate"],
852        },
853        SynonymEntry {
854            term: "dedupe",
855            targets: &["unique", "distinct", "uniq"],
856        },
857        SynonymEntry {
858            term: "shuffle",
859            targets: &["random", "randomize", "permute"],
860        },
861        // Sorting synonyms
862        SynonymEntry {
863            term: "order",
864            targets: &["sort", "sort_by", "order_by", "arrange"],
865        },
866        SynonymEntry {
867            term: "arrange",
868            targets: &["sort", "sort_by", "order_by"],
869        },
870        SynonymEntry {
871            term: "rank",
872            targets: &["sort", "sort_by", "order_by"],
873        },
874        // Math synonyms
875        SynonymEntry {
876            term: "average",
877            targets: &["avg", "mean", "arithmetic_mean"],
878        },
879        SynonymEntry {
880            term: "mean",
881            targets: &["avg", "average"],
882        },
883        SynonymEntry {
884            term: "total",
885            targets: &["sum", "add", "accumulate"],
886        },
887        SynonymEntry {
888            term: "add",
889            targets: &["sum", "plus", "addition"],
890        },
891        SynonymEntry {
892            term: "subtract",
893            targets: &["minus", "difference"],
894        },
895        SynonymEntry {
896            term: "multiply",
897            targets: &["times", "product", "mul"],
898        },
899        SynonymEntry {
900            term: "divide",
901            targets: &["quotient", "div"],
902        },
903        SynonymEntry {
904            term: "remainder",
905            targets: &["mod", "modulo", "modulus"],
906        },
907        SynonymEntry {
908            term: "power",
909            targets: &["pow", "exponent", "exp"],
910        },
911        SynonymEntry {
912            term: "absolute",
913            targets: &["abs", "magnitude"],
914        },
915        SynonymEntry {
916            term: "round",
917            targets: &["round", "round_to", "nearest"],
918        },
919        SynonymEntry {
920            term: "random",
921            targets: &["rand", "random_int", "random_float"],
922        },
923        // Date/time synonyms
924        SynonymEntry {
925            term: "date",
926            targets: &["now", "today", "parse_date", "format_date", "datetime"],
927        },
928        SynonymEntry {
929            term: "time",
930            targets: &["now", "time_now", "parse_time", "datetime"],
931        },
932        SynonymEntry {
933            term: "timestamp",
934            targets: &["now", "epoch", "unix_time", "to_epoch"],
935        },
936        SynonymEntry {
937            term: "format",
938            targets: &["format_date", "strftime", "date_format"],
939        },
940        SynonymEntry {
941            term: "parse",
942            targets: &["parse_date", "parse_time", "strptime", "from_string"],
943        },
944        // Type conversion synonyms
945        SynonymEntry {
946            term: "convert",
947            targets: &["to_string", "to_number", "to_array", "type"],
948        },
949        SynonymEntry {
950            term: "cast",
951            targets: &["to_string", "to_number", "to_bool"],
952        },
953        SynonymEntry {
954            term: "stringify",
955            targets: &["to_string", "string", "str"],
956        },
957        SynonymEntry {
958            term: "numberify",
959            targets: &["to_number", "number", "int", "float"],
960        },
961        // Object/hash synonyms
962        SynonymEntry {
963            term: "object",
964            targets: &["from_items", "to_object", "merge", "object_from_items"],
965        },
966        SynonymEntry {
967            term: "dict",
968            targets: &["from_items", "to_object", "object"],
969        },
970        SynonymEntry {
971            term: "hash",
972            targets: &["md5", "sha256", "sha1", "crc32"],
973        },
974        SynonymEntry {
975            term: "encrypt",
976            targets: &["md5", "sha256", "sha1", "hmac"],
977        },
978        SynonymEntry {
979            term: "checksum",
980            targets: &["md5", "sha256", "crc32"],
981        },
982        // Encoding synonyms
983        SynonymEntry {
984            term: "encode",
985            targets: &["base64_encode", "url_encode", "hex_encode"],
986        },
987        SynonymEntry {
988            term: "decode",
989            targets: &["base64_decode", "url_decode", "hex_decode"],
990        },
991        SynonymEntry {
992            term: "escape",
993            targets: &["url_encode", "html_escape"],
994        },
995        SynonymEntry {
996            term: "unescape",
997            targets: &["url_decode", "html_unescape"],
998        },
999        // Validation synonyms
1000        SynonymEntry {
1001            term: "check",
1002            targets: &[
1003                "is_string",
1004                "is_number",
1005                "is_array",
1006                "is_object",
1007                "validate",
1008            ],
1009        },
1010        SynonymEntry {
1011            term: "validate",
1012            targets: &["is_email", "is_url", "is_uuid", "is_valid"],
1013        },
1014        SynonymEntry {
1015            term: "test",
1016            targets: &["regex_match", "contains", "starts_with", "ends_with"],
1017        },
1018        // Null/empty handling synonyms
1019        SynonymEntry {
1020            term: "default",
1021            targets: &["coalesce", "if_null", "or_else", "default_value"],
1022        },
1023        SynonymEntry {
1024            term: "empty",
1025            targets: &["is_empty", "blank", "null"],
1026        },
1027        SynonymEntry {
1028            term: "null",
1029            targets: &["is_null", "coalesce", "not_null"],
1030        },
1031        SynonymEntry {
1032            term: "fallback",
1033            targets: &["coalesce", "default", "or_else"],
1034        },
1035        // Comparison synonyms
1036        SynonymEntry {
1037            term: "equal",
1038            targets: &["eq", "equals", "same"],
1039        },
1040        SynonymEntry {
1041            term: "compare",
1042            targets: &["eq", "lt", "gt", "lte", "gte", "cmp"],
1043        },
1044        SynonymEntry {
1045            term: "between",
1046            targets: &["range", "in_range", "clamp"],
1047        },
1048        // Misc synonyms
1049        SynonymEntry {
1050            term: "copy",
1051            targets: &["clone", "dup", "duplicate"],
1052        },
1053        SynonymEntry {
1054            term: "debug",
1055            targets: &["debug", "inspect", "dump", "print"],
1056        },
1057        SynonymEntry {
1058            term: "reverse",
1059            targets: &["reverse", "flip", "invert"],
1060        },
1061        SynonymEntry {
1062            term: "repeat",
1063            targets: &["repeat", "replicate", "times"],
1064        },
1065        SynonymEntry {
1066            term: "uuid",
1067            targets: &["uuid", "uuid4", "guid", "generate_uuid"],
1068        },
1069        SynonymEntry {
1070            term: "id",
1071            targets: &["uuid", "nanoid", "ulid", "unique_id"],
1072        },
1073    ];
1074    SYNONYMS
1075}
1076
1077/// Look up synonyms for a search term
1078///
1079/// Returns the list of related function names/keywords if the term has synonyms,
1080/// or None if not found.
1081///
1082/// # Example
1083///
1084/// ```
1085/// use jmespath_extensions::registry::lookup_synonyms;
1086///
1087/// let targets = lookup_synonyms("aggregate");
1088/// assert!(targets.is_some());
1089/// assert!(targets.unwrap().contains(&"group_by"));
1090/// ```
1091pub fn lookup_synonyms(term: &str) -> Option<&'static [&'static str]> {
1092    let term_lower = term.to_lowercase();
1093    get_synonyms()
1094        .iter()
1095        .find(|s| s.term == term_lower)
1096        .map(|s| s.targets)
1097}
1098
1099/// Expand a search query using synonyms
1100///
1101/// Takes a search query (possibly multi-word) and returns all terms that should
1102/// be searched, including the original terms and any synonym expansions.
1103///
1104/// # Example
1105///
1106/// ```
1107/// use jmespath_extensions::registry::expand_search_terms;
1108///
1109/// let terms = expand_search_terms("group aggregate");
1110/// assert!(terms.contains(&"group_by".to_string()));
1111/// assert!(terms.contains(&"sum".to_string()));
1112/// ```
1113pub fn expand_search_terms(query: &str) -> Vec<String> {
1114    let mut expanded = Vec::new();
1115
1116    for word in query.split_whitespace() {
1117        let word_lower = word.to_lowercase();
1118        expanded.push(word_lower.clone());
1119
1120        if let Some(targets) = lookup_synonyms(&word_lower) {
1121            for target in targets {
1122                let target_str = (*target).to_string();
1123                if !expanded.contains(&target_str) {
1124                    expanded.push(target_str);
1125                }
1126            }
1127        }
1128    }
1129
1130    expanded
1131}