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