1use crate::engine::error::Result;
2use crate::engine::executor::ArenaContext;
3use crate::engine::functions::filter::FilterConfig;
4use crate::engine::functions::integration::{EnrichConfig, HttpCallConfig, PublishKafkaConfig};
5use crate::engine::functions::log::LogConfig;
6use crate::engine::functions::map::MapConfig;
7use crate::engine::functions::parse::{
8 ParseConfig, execute_parse_json_in_arena, execute_parse_xml,
9};
10use crate::engine::functions::publish::{PublishConfig, execute_publish_json, execute_publish_xml};
11use crate::engine::functions::validation::ValidationConfig;
12use crate::engine::message::{Change, Message};
13use crate::engine::task_outcome::TaskOutcome;
14use datalogic_rs::Engine;
15use serde::de::DeserializeOwned;
16use serde::{Deserialize, Deserializer};
17use serde_json::Value;
18use std::any::Any;
19use std::sync::Arc;
20
21#[derive(Clone)]
30pub struct CompiledCustomInput(pub Arc<dyn Any + Send + Sync>);
31
32impl CompiledCustomInput {
33 #[inline]
36 pub fn as_any(&self) -> &(dyn Any + Send + Sync) {
37 &*self.0
38 }
39}
40
41impl std::fmt::Debug for CompiledCustomInput {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.write_str("CompiledCustomInput(<opaque>)")
44 }
45}
46
47#[derive(Debug, Clone)]
56pub enum FunctionConfig {
57 Map {
58 name: MapName,
59 input: MapConfig,
60 },
61 Validation {
62 name: ValidationName,
63 input: ValidationConfig,
64 },
65 ParseJson {
66 name: ParseJsonName,
67 input: ParseConfig,
68 },
69 ParseXml {
70 name: ParseXmlName,
71 input: ParseConfig,
72 },
73 PublishJson {
74 name: PublishJsonName,
75 input: PublishConfig,
76 },
77 PublishXml {
78 name: PublishXmlName,
79 input: PublishConfig,
80 },
81 Filter {
82 name: FilterName,
83 input: FilterConfig,
84 },
85 Log {
86 name: LogName,
87 input: LogConfig,
88 },
89 HttpCall {
90 name: HttpCallName,
91 input: HttpCallConfig,
92 },
93 Enrich {
94 name: EnrichName,
95 input: EnrichConfig,
96 },
97 PublishKafka {
98 name: PublishKafkaName,
99 input: PublishKafkaConfig,
100 },
101 Custom {
104 name: String,
105 input: Value,
106 compiled_input: Option<CompiledCustomInput>,
112 },
113}
114
115#[derive(Debug, Clone, Deserialize)]
116#[serde(rename_all = "lowercase")]
117pub enum MapName {
118 Map,
119}
120
121#[derive(Debug, Clone, Deserialize, PartialEq)]
122#[serde(rename_all = "lowercase")]
123pub enum ValidationName {
124 Validation,
125 Validate,
126}
127
128#[derive(Debug, Clone, Deserialize, PartialEq)]
129#[serde(rename_all = "snake_case")]
130pub enum ParseJsonName {
131 ParseJson,
132}
133
134#[derive(Debug, Clone, Deserialize, PartialEq)]
135#[serde(rename_all = "snake_case")]
136pub enum ParseXmlName {
137 ParseXml,
138}
139
140#[derive(Debug, Clone, Deserialize, PartialEq)]
141#[serde(rename_all = "snake_case")]
142pub enum PublishJsonName {
143 PublishJson,
144}
145
146#[derive(Debug, Clone, Deserialize, PartialEq)]
147#[serde(rename_all = "snake_case")]
148pub enum PublishXmlName {
149 PublishXml,
150}
151
152#[derive(Debug, Clone, Deserialize, PartialEq)]
153#[serde(rename_all = "lowercase")]
154pub enum FilterName {
155 Filter,
156}
157
158#[derive(Debug, Clone, Deserialize, PartialEq)]
159#[serde(rename_all = "lowercase")]
160pub enum LogName {
161 Log,
162}
163
164#[derive(Debug, Clone, Deserialize, PartialEq)]
165#[serde(rename_all = "snake_case")]
166pub enum HttpCallName {
167 HttpCall,
168}
169
170#[derive(Debug, Clone, Deserialize, PartialEq)]
171#[serde(rename_all = "snake_case")]
172pub enum EnrichName {
173 Enrich,
174}
175
176#[derive(Debug, Clone, Deserialize, PartialEq)]
177#[serde(rename_all = "snake_case")]
178pub enum PublishKafkaName {
179 PublishKafka,
180}
181
182pub const BUILTIN_FUNCTION_NAMES: &[&str] = &[
202 "map",
203 "validation",
204 "validate",
205 "parse_json",
206 "parse_xml",
207 "publish_json",
208 "publish_xml",
209 "filter",
210 "log",
211 "http_call",
212 "enrich",
213 "publish_kafka",
214];
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum BuiltinKind {
228 SelfContained,
230 RequiresHandler,
239}
240
241pub fn builtin_function_kind(name: &str) -> Option<BuiltinKind> {
257 match name {
258 "map" | "validation" | "validate" | "parse_json" | "parse_xml" | "publish_json"
259 | "publish_xml" | "filter" | "log" => Some(BuiltinKind::SelfContained),
260 "http_call" | "enrich" | "publish_kafka" => Some(BuiltinKind::RequiresHandler),
261 _ => None,
262 }
263}
264
265#[inline]
272pub fn is_builtin_function(name: &str) -> bool {
273 builtin_function_kind(name).is_some()
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct DispatchableFunction<'a> {
285 pub name: &'a str,
288 pub kind: Option<BuiltinKind>,
297 pub aliases: &'static [&'static str],
304}
305
306const VALIDATE_ALIASES: &[&str] = &["validation"];
310
311const NO_ALIASES: &[&str] = &[];
314
315pub(crate) fn canonical_builtin_name(name: &str) -> &str {
322 match name {
323 "validation" => "validate",
324 other => other,
325 }
326}
327
328pub(crate) fn builtin_aliases(canonical: &str) -> &'static [&'static str] {
332 match canonical {
333 "validate" => VALIDATE_ALIASES,
334 _ => NO_ALIASES,
335 }
336}
337
338pub(crate) fn can_dispatch_in<V>(
350 registry: &std::collections::HashMap<String, V>,
351 name: &str,
352) -> bool {
353 match builtin_function_kind(name) {
354 Some(BuiltinKind::SelfContained) => true,
355 _ => registry.contains_key(name),
357 }
358}
359
360pub(crate) fn dispatchable_functions_in<V>(
372 registry: &std::collections::HashMap<String, V>,
373) -> impl Iterator<Item = DispatchableFunction<'_>> {
374 let builtins = BUILTIN_FUNCTION_NAMES
375 .iter()
376 .copied()
377 .filter(|name| canonical_builtin_name(name) == *name)
379 .filter_map(move |name| match builtin_function_kind(name) {
380 kind @ Some(BuiltinKind::SelfContained) => Some(DispatchableFunction {
382 name,
383 kind,
384 aliases: builtin_aliases(name),
385 }),
386 kind @ Some(BuiltinKind::RequiresHandler) if registry.contains_key(name) => {
388 Some(DispatchableFunction {
389 name,
390 kind,
391 aliases: builtin_aliases(name),
392 })
393 }
394 _ => None,
395 });
396
397 let customs = registry
398 .keys()
399 .map(String::as_str)
400 .filter(|name| builtin_function_kind(name).is_none())
403 .map(|name| DispatchableFunction {
404 name,
405 kind: None,
406 aliases: NO_ALIASES,
407 });
408
409 builtins.chain(customs)
410}
411
412fn parse_function_input<T, E>(func: &str, input: Value) -> std::result::Result<T, E>
419where
420 T: DeserializeOwned,
421 E: serde::de::Error,
422{
423 serde_json::from_value::<T>(input).map_err(|err| {
424 let raw = err.to_string();
425 let trimmed = raw
426 .rsplit_once(" at line ")
427 .map(|(head, _)| head)
428 .unwrap_or(&raw);
429 E::custom(format!("config for function '{func}': {trimmed}"))
430 })
431}
432
433impl<'de> Deserialize<'de> for FunctionConfig {
434 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
435 where
436 D: Deserializer<'de>,
437 {
438 #[derive(Deserialize)]
442 struct Raw {
443 name: String,
444 input: Value,
445 }
446
447 let Raw { name, input } = Raw::deserialize(deserializer)?;
448
449 Ok(match name.as_str() {
450 "map" => FunctionConfig::Map {
451 name: MapName::Map,
452 input: parse_function_input("map", input)?,
453 },
454 "validate" => FunctionConfig::Validation {
455 name: ValidationName::Validate,
456 input: parse_function_input("validate", input)?,
457 },
458 "validation" => FunctionConfig::Validation {
459 name: ValidationName::Validation,
460 input: parse_function_input("validation", input)?,
461 },
462 "parse_json" => FunctionConfig::ParseJson {
463 name: ParseJsonName::ParseJson,
464 input: parse_function_input("parse_json", input)?,
465 },
466 "parse_xml" => FunctionConfig::ParseXml {
467 name: ParseXmlName::ParseXml,
468 input: parse_function_input("parse_xml", input)?,
469 },
470 "publish_json" => FunctionConfig::PublishJson {
471 name: PublishJsonName::PublishJson,
472 input: parse_function_input("publish_json", input)?,
473 },
474 "publish_xml" => FunctionConfig::PublishXml {
475 name: PublishXmlName::PublishXml,
476 input: parse_function_input("publish_xml", input)?,
477 },
478 "filter" => FunctionConfig::Filter {
479 name: FilterName::Filter,
480 input: parse_function_input("filter", input)?,
481 },
482 "log" => FunctionConfig::Log {
483 name: LogName::Log,
484 input: parse_function_input("log", input)?,
485 },
486 "http_call" => FunctionConfig::HttpCall {
487 name: HttpCallName::HttpCall,
488 input: parse_function_input("http_call", input)?,
489 },
490 "enrich" => FunctionConfig::Enrich {
491 name: EnrichName::Enrich,
492 input: parse_function_input("enrich", input)?,
493 },
494 "publish_kafka" => FunctionConfig::PublishKafka {
495 name: PublishKafkaName::PublishKafka,
496 input: parse_function_input("publish_kafka", input)?,
497 },
498 _ => FunctionConfig::Custom {
499 name,
500 input,
501 compiled_input: None,
502 },
503 })
504 }
505}
506
507fn refresh_data_on_success(
514 message: &Message,
515 arena_ctx: &mut ArenaContext<'_>,
516 result: Result<(TaskOutcome, Vec<Change>)>,
517) -> Result<(TaskOutcome, Vec<Change>)> {
518 if result.is_ok() {
519 arena_ctx.refresh_for_path(&message.context, "data");
520 }
521 result
522}
523
524impl FunctionConfig {
525 pub fn function_name(&self) -> &str {
527 match self {
528 FunctionConfig::Map { .. } => "map",
529 FunctionConfig::Validation { .. } => "validate",
530 FunctionConfig::ParseJson { .. } => "parse_json",
531 FunctionConfig::ParseXml { .. } => "parse_xml",
532 FunctionConfig::PublishJson { .. } => "publish_json",
533 FunctionConfig::PublishXml { .. } => "publish_xml",
534 FunctionConfig::Filter { .. } => "filter",
535 FunctionConfig::Log { .. } => "log",
536 FunctionConfig::HttpCall { .. } => "http_call",
537 FunctionConfig::Enrich { .. } => "enrich",
538 FunctionConfig::PublishKafka { .. } => "publish_kafka",
539 FunctionConfig::Custom { name, .. } => name,
540 }
541 }
542
543 pub fn connector(&self) -> Option<&str> {
569 match self {
570 FunctionConfig::HttpCall { input, .. } => Some(&input.connector),
571 FunctionConfig::Enrich { input, .. } => Some(&input.connector),
572 FunctionConfig::PublishKafka { input, .. } => Some(&input.connector),
573 FunctionConfig::Custom { input, .. } => input.get("connector").and_then(Value::as_str),
574 FunctionConfig::Map { .. }
575 | FunctionConfig::Validation { .. }
576 | FunctionConfig::ParseJson { .. }
577 | FunctionConfig::ParseXml { .. }
578 | FunctionConfig::PublishJson { .. }
579 | FunctionConfig::PublishXml { .. }
580 | FunctionConfig::Filter { .. }
581 | FunctionConfig::Log { .. } => None,
582 }
583 }
584
585 pub fn is_sync_builtin(&self) -> bool {
586 matches!(
587 self,
588 FunctionConfig::Map { .. }
589 | FunctionConfig::Validation { .. }
590 | FunctionConfig::ParseJson { .. }
591 | FunctionConfig::ParseXml { .. }
592 | FunctionConfig::PublishJson { .. }
593 | FunctionConfig::PublishXml { .. }
594 | FunctionConfig::Filter { .. }
595 | FunctionConfig::Log { .. }
596 )
597 }
598
599 pub(crate) fn try_execute_in_arena<'arena>(
613 &'arena self,
614 message: &mut Message,
615 arena_ctx: &mut ArenaContext<'arena>,
616 engine: &Arc<Engine>,
617 mapping_snapshots: Option<&mut Vec<Value>>,
618 ) -> Option<Result<(TaskOutcome, Vec<Change>)>> {
619 match self {
620 FunctionConfig::Map { input, .. } => {
621 Some(input.execute_in_arena(message, arena_ctx, engine, mapping_snapshots))
622 }
623 FunctionConfig::Validation { input, .. } => {
624 Some(input.execute_in_arena(message, arena_ctx, engine))
625 }
626 FunctionConfig::ParseJson { input, .. } => {
627 Some(execute_parse_json_in_arena(message, input, arena_ctx))
628 }
629 FunctionConfig::ParseXml { input, .. } => {
630 let result = execute_parse_xml(message, input);
636 Some(refresh_data_on_success(message, arena_ctx, result))
637 }
638 FunctionConfig::PublishJson { input, .. } => {
639 let result = execute_publish_json(message, input);
640 Some(refresh_data_on_success(message, arena_ctx, result))
641 }
642 FunctionConfig::PublishXml { input, .. } => {
643 let result = execute_publish_xml(message, input);
644 Some(refresh_data_on_success(message, arena_ctx, result))
645 }
646 FunctionConfig::Filter { input, .. } => {
647 Some(input.execute_in_arena(message, arena_ctx, engine))
648 }
649 FunctionConfig::Log { input, .. } => {
650 Some(input.execute_in_arena(message, arena_ctx, engine))
651 }
652 FunctionConfig::HttpCall { .. }
653 | FunctionConfig::Enrich { .. }
654 | FunctionConfig::PublishKafka { .. }
655 | FunctionConfig::Custom { .. } => None,
656 }
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663 use serde_json::json;
664
665 fn parse(value: serde_json::Value) -> std::result::Result<FunctionConfig, serde_json::Error> {
666 serde_json::from_value(value)
667 }
668
669 #[test]
670 fn map_with_valid_config_deserializes_to_map_variant() {
671 let cfg = parse(json!({
672 "name": "map",
673 "input": {
674 "mappings": [
675 { "path": "data.x", "logic": { "var": "data.y" } }
676 ]
677 }
678 }))
679 .expect("valid map config should deserialize");
680 assert!(matches!(cfg, FunctionConfig::Map { .. }));
681 }
682
683 #[test]
684 fn map_with_missing_mappings_gives_clear_error() {
685 let err = parse(json!({
686 "name": "map",
687 "input": {}
688 }))
689 .expect_err("map with empty input should fail");
690 let msg = err.to_string();
691 assert!(
692 msg.starts_with("config for function 'map':"),
693 "error should be prefixed with function envelope, got: {msg}"
694 );
695 assert!(
696 msg.contains("mappings"),
697 "error should mention the missing field, got: {msg}"
698 );
699 }
700
701 #[test]
702 fn map_with_wrong_input_shape_gives_clear_error() {
703 let err = parse(json!({
704 "name": "map",
705 "input": { "mappings": "not an array" }
706 }))
707 .expect_err("map with bad mappings type should fail");
708 let msg = err.to_string();
709 assert!(
710 msg.starts_with("config for function 'map':"),
711 "error should be prefixed with function envelope, got: {msg}"
712 );
713 }
714
715 #[test]
716 fn validation_accepts_both_spellings() {
717 for name in ["validate", "validation"] {
718 let cfg = parse(json!({
719 "name": name,
720 "input": { "rules": [] }
721 }))
722 .unwrap_or_else(|e| panic!("'{name}' should deserialize: {e}"));
723 assert!(matches!(cfg, FunctionConfig::Validation { .. }));
724 }
725 }
726
727 #[test]
728 fn unknown_name_falls_through_to_custom() {
729 let cfg = parse(json!({
730 "name": "my_custom_handler",
731 "input": { "anything": "goes" }
732 }))
733 .expect("unknown name should produce Custom");
734 match cfg {
735 FunctionConfig::Custom {
736 name,
737 compiled_input,
738 ..
739 } => {
740 assert_eq!(name, "my_custom_handler");
741 assert!(compiled_input.is_none());
742 }
743 other => panic!("expected Custom, got {other:?}"),
744 }
745 }
746
747 #[test]
748 fn missing_name_field_errors() {
749 let err = parse(json!({ "input": {} })).expect_err("missing name should fail");
750 assert!(err.to_string().contains("name"));
751 }
752
753 #[test]
754 fn missing_input_field_errors() {
755 let err = parse(json!({ "name": "map" })).expect_err("missing input should fail");
756 assert!(err.to_string().contains("input"));
757 }
758
759 #[test]
760 fn http_call_with_missing_connector_gives_clear_error() {
761 let err = parse(json!({
762 "name": "http_call",
763 "input": { "method": "GET" }
764 }))
765 .expect_err("http_call needs connector");
766 let msg = err.to_string();
767 assert!(
768 msg.starts_with("config for function 'http_call':"),
769 "error should be prefixed with function envelope, got: {msg}"
770 );
771 assert!(msg.contains("connector"));
772 }
773
774 #[test]
775 fn builtin_names_never_fall_through_to_custom() {
776 for name in BUILTIN_FUNCTION_NAMES {
780 let cfg = parse(json!({
781 "name": name,
782 "input": {}
783 }));
784 match cfg {
785 Ok(c) => assert!(
786 !matches!(c, FunctionConfig::Custom { .. }),
787 "name '{name}' silently fell through to Custom"
788 ),
789 Err(e) => assert!(
790 e.to_string()
791 .starts_with(&format!("config for function '{name}':")),
792 "name '{name}' failed without envelope: {e}"
793 ),
794 }
795
796 assert!(
799 builtin_function_kind(name).is_some(),
800 "name '{name}' is in BUILTIN_FUNCTION_NAMES but classifies as None"
801 );
802 }
803 }
804
805 fn parse_http_call(
807 input: serde_json::Value,
808 ) -> std::result::Result<HttpCallConfig, serde_json::Error> {
809 match parse(json!({ "name": "http_call", "input": input }))? {
810 FunctionConfig::HttpCall { input, .. } => Ok(input),
811 other => panic!("expected HttpCall, got {other:?}"),
812 }
813 }
814
815 #[test]
816 fn http_call_response_path_is_read_under_its_own_name() {
817 let cfg = parse_http_call(json!({ "connector": "c", "response_path": "data.x" }))
818 .expect("response_path should parse");
819 assert_eq!(cfg.response_path.as_deref(), Some("data.x"));
820 }
821
822 #[test]
823 fn http_call_response_path_accepts_the_output_alias() {
824 let cfg = parse_http_call(json!({ "connector": "c", "output": "data.x" }))
827 .expect("output should be accepted as an alias");
828 assert_eq!(cfg.response_path.as_deref(), Some("data.x"));
829 }
830
831 #[test]
832 fn http_call_response_path_is_optional() {
833 let cfg = parse_http_call(json!({ "connector": "c" })).expect("no destination is valid");
834 assert_eq!(cfg.response_path, None);
835 }
836
837 #[test]
838 fn http_call_rejects_both_destination_keys_in_either_order() {
839 for input in [
842 json!({ "connector": "c", "response_path": "a", "output": "b" }),
843 json!({ "connector": "c", "output": "b", "response_path": "a" }),
844 ] {
845 let err = parse_http_call(input.clone())
846 .expect_err("supplying both destination keys must fail");
847 let msg = err.to_string();
848 assert!(
849 msg.starts_with("config for function 'http_call':"),
850 "error should carry the function envelope, got: {msg}"
851 );
852 assert!(
853 msg.contains("duplicate field"),
854 "error should name the conflict, got: {msg}"
855 );
856 }
857 }
858
859 #[test]
860 fn http_call_rejects_a_misspelled_destination_field() {
861 for bad in ["outputs", "Output", "respose_path", "response-path"] {
865 let mut input = serde_json::Map::new();
866 input.insert("connector".to_string(), json!("c"));
867 input.insert(bad.to_string(), json!("data.x"));
868
869 let err = parse_http_call(serde_json::Value::Object(input))
870 .expect_err("a misspelled field must be rejected, not silently discarded");
871 let msg = err.to_string();
872 assert!(
873 msg.starts_with("config for function 'http_call':"),
874 "error should carry the function envelope, got: {msg}"
875 );
876 assert!(
877 msg.contains("unknown field"),
878 "error should say the field is unknown, got: {msg}"
879 );
880 assert!(
881 msg.contains(bad),
882 "error should name the offending field '{bad}', got: {msg}"
883 );
884 }
885 }
886
887 #[test]
888 fn enrich_does_not_accept_the_output_alias() {
889 let err = parse(json!({
893 "name": "enrich",
894 "input": { "connector": "c", "output": "data.x" }
895 }))
896 .expect_err("enrich has no `output` field");
897 let msg = err.to_string();
898 assert!(
899 msg.starts_with("config for function 'enrich':"),
900 "error should carry the function envelope, got: {msg}"
901 );
902
903 let ok = parse(json!({
905 "name": "enrich",
906 "input": { "connector": "c", "merge_path": "data.x" }
907 }))
908 .expect("merge_path is enrich's destination field");
909 assert!(matches!(ok, FunctionConfig::Enrich { .. }));
910 }
911
912 #[test]
913 fn publish_kafka_rejects_unknown_fields() {
914 let err = parse(json!({
915 "name": "publish_kafka",
916 "input": { "connector": "c", "topic": "t", "tpoic": "typo" }
917 }))
918 .expect_err("publish_kafka should reject an unknown field");
919 assert!(err.to_string().contains("unknown field"), "got: {err}");
920 }
921
922 #[test]
923 fn connector_is_returned_for_the_three_typed_integrations() {
924 let cases = [
925 (
926 json!({ "name": "http_call", "input": { "connector": "user_service" } }),
927 "user_service",
928 ),
929 (
930 json!({ "name": "enrich",
931 "input": { "connector": "ref_data", "merge_path": "data.out" } }),
932 "ref_data",
933 ),
934 (
935 json!({ "name": "publish_kafka",
936 "input": { "connector": "events", "topic": "t" } }),
937 "events",
938 ),
939 ];
940 for (input, expected) in cases {
941 let cfg = parse(input.clone()).expect("should parse");
942 assert_eq!(cfg.connector(), Some(expected), "for {input}");
943 }
944 }
945
946 #[test]
947 fn connector_is_none_for_every_non_connector_builtin() {
948 let minimal_input = |name: &str| -> serde_json::Value {
951 match name {
952 "map" => json!({ "mappings": [] }),
953 "validation" | "validate" => json!({ "rules": [] }),
954 "parse_json" | "parse_xml" | "publish_json" | "publish_xml" => {
955 json!({ "source": "data.in", "target": "out" })
956 }
957 "filter" => json!({ "condition": true }),
958 "log" => json!({ "message": "hi" }),
959 _ => json!({}),
960 }
961 };
962
963 for name in BUILTIN_FUNCTION_NAMES {
964 if matches!(*name, "http_call" | "enrich" | "publish_kafka") {
965 continue;
966 }
967 let cfg = parse(json!({ "name": name, "input": minimal_input(name) }))
968 .unwrap_or_else(|e| panic!("'{name}' should parse: {e}"));
969 assert_eq!(cfg.connector(), None, "'{name}' names no connector");
970 }
971 }
972
973 #[test]
974 fn connector_reads_the_custom_convention() {
975 let cfg = parse(json!({
976 "name": "pg_query",
977 "input": { "connector": "pg_main", "database": "orders" }
978 }))
979 .unwrap();
980 assert_eq!(cfg.connector(), Some("pg_main"));
981 }
982
983 #[test]
984 fn connector_is_none_for_a_custom_input_without_a_string_connector() {
985 for input in [
987 json!({}), json!({ "connector": 7 }), json!({ "connector": true }), json!({ "connector": null }), json!({ "connector": ["a"] }), json!({ "connector": { "n": "a" } }), json!([]), json!(7), ] {
996 let cfg = parse(json!({ "name": "my_handler", "input": input.clone() }))
997 .unwrap_or_else(|e| panic!("custom should parse {input}: {e}"));
998 assert_eq!(cfg.connector(), None, "for input {input}");
999 }
1000 }
1001
1002 #[test]
1003 fn connector_returns_an_empty_name_verbatim() {
1004 let typed = parse(json!({ "name": "http_call", "input": { "connector": "" } })).unwrap();
1008 assert_eq!(typed.connector(), Some(""));
1009
1010 let custom = parse(json!({ "name": "x", "input": { "connector": "" } })).unwrap();
1011 assert_eq!(custom.connector(), Some(""));
1012 }
1013
1014 #[test]
1015 fn connector_returns_a_non_ascii_name_byte_for_byte() {
1016 let cfg =
1018 parse(json!({ "name": "http_call", "input": { "connector": "連携先" } })).unwrap();
1019 assert_eq!(cfg.connector(), Some("連携先"));
1020 }
1021
1022 #[test]
1023 fn builtin_function_kind_is_none_for_non_builtins() {
1024 for name in [
1026 "",
1027 "__not_a_builtin__",
1028 "HTTP_CALL", "htttp_call", "map ", "publish_kafk", ] {
1033 assert_eq!(
1034 builtin_function_kind(name),
1035 None,
1036 "'{name}' must not classify as a built-in"
1037 );
1038 assert!(!is_builtin_function(name));
1039 }
1040 }
1041
1042 #[test]
1043 fn builtin_kinds_partition_matches_real_dispatch_behaviour() {
1044 let minimal_input = |name: &str| -> serde_json::Value {
1051 match name {
1052 "map" => json!({ "mappings": [] }),
1053 "validation" | "validate" => json!({ "rules": [] }),
1054 "parse_json" | "parse_xml" | "publish_json" | "publish_xml" => {
1055 json!({ "source": "data.in", "target": "out" })
1056 }
1057 "filter" => json!({ "condition": true }),
1058 "log" => json!({ "message": "hi" }),
1059 "http_call" => json!({ "connector": "c" }),
1060 "enrich" => json!({ "connector": "c", "merge_path": "data.out" }),
1061 "publish_kafka" => json!({ "connector": "c", "topic": "t" }),
1062 _ => json!({}),
1065 }
1066 };
1067
1068 for name in BUILTIN_FUNCTION_NAMES {
1069 let kind = builtin_function_kind(name)
1070 .unwrap_or_else(|| panic!("'{name}' must classify as a built-in"));
1071 let cfg = parse(json!({ "name": name, "input": minimal_input(name) }))
1072 .unwrap_or_else(|e| panic!("'{name}' should parse with minimal input: {e}"));
1073
1074 assert_eq!(
1075 cfg.is_sync_builtin(),
1076 matches!(kind, BuiltinKind::SelfContained),
1077 "'{name}' classifies as {kind:?} but is_sync_builtin() is {}",
1078 cfg.is_sync_builtin()
1079 );
1080 }
1081 }
1082
1083 #[test]
1084 fn requires_handler_kind_covers_exactly_the_config_only_integrations() {
1085 for name in ["http_call", "enrich", "publish_kafka"] {
1087 assert_eq!(
1088 builtin_function_kind(name),
1089 Some(BuiltinKind::RequiresHandler),
1090 "'{name}' ships as config only and needs a registered handler"
1091 );
1092 }
1093
1094 for name in [
1098 "map",
1099 "validation",
1100 "validate",
1101 "parse_json",
1102 "parse_xml",
1103 "publish_json",
1104 "publish_xml",
1105 "filter",
1106 "log",
1107 ] {
1108 assert_eq!(
1109 builtin_function_kind(name),
1110 Some(BuiltinKind::SelfContained),
1111 "'{name}' is executed by this crate"
1112 );
1113 }
1114 }
1115}
1116
1117#[cfg(test)]
1118mod dispatch_vocabulary_tests {
1119 use super::*;
1120 use std::collections::HashMap;
1121
1122 fn registry(names: &[&str]) -> HashMap<String, ()> {
1125 names.iter().map(|n| ((*n).to_string(), ())).collect()
1126 }
1127
1128 fn names(registry: &HashMap<String, ()>) -> Vec<&str> {
1129 let mut out: Vec<&str> = dispatchable_functions_in(registry)
1130 .map(|f| f.name)
1131 .collect();
1132 out.sort_unstable();
1133 out
1134 }
1135
1136 #[test]
1143 fn aliases_and_canonical_names_agree() {
1144 for name in BUILTIN_FUNCTION_NAMES {
1147 let canonical = canonical_builtin_name(name);
1148 assert_eq!(
1149 canonical_builtin_name(canonical),
1150 canonical,
1151 "'{name}' resolves to '{canonical}', which must itself be canonical"
1152 );
1153 assert!(
1154 BUILTIN_FUNCTION_NAMES.contains(&canonical),
1155 "'{canonical}' is a canonical name and must be an accepted spelling"
1156 );
1157 assert_eq!(
1160 builtin_function_kind(name),
1161 builtin_function_kind(canonical),
1162 "'{name}' and '{canonical}' are one function and must classify alike"
1163 );
1164 }
1165
1166 for name in BUILTIN_FUNCTION_NAMES {
1169 let is_canonical = canonical_builtin_name(name) == *name;
1170 let alias_of: Vec<&str> = BUILTIN_FUNCTION_NAMES
1171 .iter()
1172 .copied()
1173 .filter(|c| builtin_aliases(c).contains(name))
1174 .collect();
1175 assert_eq!(
1176 is_canonical,
1177 alias_of.is_empty(),
1178 "'{name}' must be canonical XOR an alias, got canonical={is_canonical} \
1179 listed-as-alias-of={alias_of:?}"
1180 );
1181 assert!(
1182 alias_of.len() <= 1,
1183 "'{name}' is listed as an alias of more than one function: {alias_of:?}"
1184 );
1185 }
1186
1187 for canonical in BUILTIN_FUNCTION_NAMES {
1189 for alias in builtin_aliases(canonical) {
1190 assert_eq!(
1191 canonical_builtin_name(alias),
1192 *canonical,
1193 "'{alias}' is listed under '{canonical}' but does not resolve to it"
1194 );
1195 }
1196 }
1197 }
1198
1199 #[test]
1200 fn validate_is_canonical_and_validation_is_its_alias() {
1201 assert_eq!(canonical_builtin_name("validation"), "validate");
1202 assert_eq!(canonical_builtin_name("validate"), "validate");
1203 assert_eq!(builtin_aliases("validate"), &["validation"]);
1204 assert!(builtin_aliases("validation").is_empty());
1205 assert!(builtin_aliases("map").is_empty());
1206 }
1207
1208 #[test]
1209 fn an_empty_registry_dispatches_every_self_contained_builtin() {
1210 assert_eq!(
1211 names(®istry(&[])),
1212 vec![
1213 "filter",
1214 "log",
1215 "map",
1216 "parse_json",
1217 "parse_xml",
1218 "publish_json",
1219 "publish_xml",
1220 "validate",
1221 ],
1222 "self-contained built-ins need no registration; `validation` is \
1223 folded into `validate`, and the three config-only integrations are absent"
1224 );
1225 }
1226
1227 #[test]
1228 fn requires_handler_builtins_appear_only_when_registered() {
1229 let empty = registry(&[]);
1230 assert!(!names(&empty).contains(&"enrich"));
1231 assert!(!can_dispatch_in(&empty, "enrich"));
1232
1233 let backed = registry(&["enrich"]);
1234 assert!(names(&backed).contains(&"enrich"));
1235 assert!(can_dispatch_in(&backed, "enrich"));
1236
1237 let entry = dispatchable_functions_in(&backed)
1239 .find(|f| f.name == "enrich")
1240 .expect("registered enrich is enumerated");
1241 assert_eq!(entry.kind, Some(BuiltinKind::RequiresHandler));
1242 }
1243
1244 #[test]
1245 fn custom_names_are_enumerated_with_no_kind() {
1246 let reg = registry(&["shout"]);
1247 let entry = dispatchable_functions_in(®)
1248 .find(|f| f.name == "shout")
1249 .expect("a registered custom name is enumerated");
1250 assert_eq!(entry.kind, None, "None is how a custom handler reports");
1251 assert!(entry.aliases.is_empty());
1252 assert!(can_dispatch_in(®, "shout"));
1253 assert!(!can_dispatch_in(®istry(&[]), "shout"));
1254 }
1255
1256 #[test]
1257 fn registering_a_self_contained_name_is_inert_and_never_duplicates_it() {
1258 let shadowed = registry(&["map"]);
1262 assert_eq!(
1263 names(&shadowed),
1264 names(®istry(&[])),
1265 "a shadowing registration changes nothing about the vocabulary"
1266 );
1267 assert_eq!(
1268 dispatchable_functions_in(&shadowed)
1269 .filter(|f| f.name == "map")
1270 .count(),
1271 1,
1272 "`map` is yielded exactly once, not once per source"
1273 );
1274 }
1275
1276 #[test]
1277 fn aliases_dispatch_but_are_not_enumerated() {
1278 let reg = registry(&[]);
1279 assert!(
1280 can_dispatch_in(®, "validation"),
1281 "a task named `validation` really does execute"
1282 );
1283 assert!(
1284 !names(®).contains(&"validation"),
1285 "but the enumeration reports it under `validate`"
1286 );
1287 }
1288
1289 #[test]
1290 fn can_dispatch_rejects_names_the_crate_does_not_know() {
1291 let reg = registry(&["shout"]);
1292 assert!(!can_dispatch_in(®, "SHOUT"), "matching is exact");
1293 assert!(!can_dispatch_in(®, "htttp_call"));
1294 assert!(!can_dispatch_in(®, ""));
1295 }
1296
1297 #[test]
1300 fn every_enumerated_name_is_dispatchable() {
1301 let reg = registry(&["enrich", "shout"]);
1302 for f in dispatchable_functions_in(®) {
1303 assert!(
1304 can_dispatch_in(®, f.name),
1305 "'{}' is enumerated, so it must dispatch",
1306 f.name
1307 );
1308 for alias in f.aliases {
1309 assert!(
1310 can_dispatch_in(®, alias),
1311 "alias '{alias}' of '{}' must dispatch too",
1312 f.name
1313 );
1314 }
1315 }
1316 }
1317}