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