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
276fn parse_function_input<T, E>(func: &str, input: Value) -> std::result::Result<T, E>
283where
284 T: DeserializeOwned,
285 E: serde::de::Error,
286{
287 serde_json::from_value::<T>(input).map_err(|err| {
288 let raw = err.to_string();
289 let trimmed = raw
290 .rsplit_once(" at line ")
291 .map(|(head, _)| head)
292 .unwrap_or(&raw);
293 E::custom(format!("config for function '{func}': {trimmed}"))
294 })
295}
296
297impl<'de> Deserialize<'de> for FunctionConfig {
298 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
299 where
300 D: Deserializer<'de>,
301 {
302 #[derive(Deserialize)]
306 struct Raw {
307 name: String,
308 input: Value,
309 }
310
311 let Raw { name, input } = Raw::deserialize(deserializer)?;
312
313 Ok(match name.as_str() {
314 "map" => FunctionConfig::Map {
315 name: MapName::Map,
316 input: parse_function_input("map", input)?,
317 },
318 "validate" => FunctionConfig::Validation {
319 name: ValidationName::Validate,
320 input: parse_function_input("validate", input)?,
321 },
322 "validation" => FunctionConfig::Validation {
323 name: ValidationName::Validation,
324 input: parse_function_input("validation", input)?,
325 },
326 "parse_json" => FunctionConfig::ParseJson {
327 name: ParseJsonName::ParseJson,
328 input: parse_function_input("parse_json", input)?,
329 },
330 "parse_xml" => FunctionConfig::ParseXml {
331 name: ParseXmlName::ParseXml,
332 input: parse_function_input("parse_xml", input)?,
333 },
334 "publish_json" => FunctionConfig::PublishJson {
335 name: PublishJsonName::PublishJson,
336 input: parse_function_input("publish_json", input)?,
337 },
338 "publish_xml" => FunctionConfig::PublishXml {
339 name: PublishXmlName::PublishXml,
340 input: parse_function_input("publish_xml", input)?,
341 },
342 "filter" => FunctionConfig::Filter {
343 name: FilterName::Filter,
344 input: parse_function_input("filter", input)?,
345 },
346 "log" => FunctionConfig::Log {
347 name: LogName::Log,
348 input: parse_function_input("log", input)?,
349 },
350 "http_call" => FunctionConfig::HttpCall {
351 name: HttpCallName::HttpCall,
352 input: parse_function_input("http_call", input)?,
353 },
354 "enrich" => FunctionConfig::Enrich {
355 name: EnrichName::Enrich,
356 input: parse_function_input("enrich", input)?,
357 },
358 "publish_kafka" => FunctionConfig::PublishKafka {
359 name: PublishKafkaName::PublishKafka,
360 input: parse_function_input("publish_kafka", input)?,
361 },
362 _ => FunctionConfig::Custom {
363 name,
364 input,
365 compiled_input: None,
366 },
367 })
368 }
369}
370
371fn refresh_data_on_success(
378 message: &Message,
379 arena_ctx: &mut ArenaContext<'_>,
380 result: Result<(TaskOutcome, Vec<Change>)>,
381) -> Result<(TaskOutcome, Vec<Change>)> {
382 if result.is_ok() {
383 arena_ctx.refresh_for_path(&message.context, "data");
384 }
385 result
386}
387
388impl FunctionConfig {
389 pub fn function_name(&self) -> &str {
391 match self {
392 FunctionConfig::Map { .. } => "map",
393 FunctionConfig::Validation { .. } => "validate",
394 FunctionConfig::ParseJson { .. } => "parse_json",
395 FunctionConfig::ParseXml { .. } => "parse_xml",
396 FunctionConfig::PublishJson { .. } => "publish_json",
397 FunctionConfig::PublishXml { .. } => "publish_xml",
398 FunctionConfig::Filter { .. } => "filter",
399 FunctionConfig::Log { .. } => "log",
400 FunctionConfig::HttpCall { .. } => "http_call",
401 FunctionConfig::Enrich { .. } => "enrich",
402 FunctionConfig::PublishKafka { .. } => "publish_kafka",
403 FunctionConfig::Custom { name, .. } => name,
404 }
405 }
406
407 pub fn connector(&self) -> Option<&str> {
433 match self {
434 FunctionConfig::HttpCall { input, .. } => Some(&input.connector),
435 FunctionConfig::Enrich { input, .. } => Some(&input.connector),
436 FunctionConfig::PublishKafka { input, .. } => Some(&input.connector),
437 FunctionConfig::Custom { input, .. } => input.get("connector").and_then(Value::as_str),
438 FunctionConfig::Map { .. }
439 | FunctionConfig::Validation { .. }
440 | FunctionConfig::ParseJson { .. }
441 | FunctionConfig::ParseXml { .. }
442 | FunctionConfig::PublishJson { .. }
443 | FunctionConfig::PublishXml { .. }
444 | FunctionConfig::Filter { .. }
445 | FunctionConfig::Log { .. } => None,
446 }
447 }
448
449 pub fn is_sync_builtin(&self) -> bool {
450 matches!(
451 self,
452 FunctionConfig::Map { .. }
453 | FunctionConfig::Validation { .. }
454 | FunctionConfig::ParseJson { .. }
455 | FunctionConfig::ParseXml { .. }
456 | FunctionConfig::PublishJson { .. }
457 | FunctionConfig::PublishXml { .. }
458 | FunctionConfig::Filter { .. }
459 | FunctionConfig::Log { .. }
460 )
461 }
462
463 pub(crate) fn try_execute_in_arena<'arena>(
477 &'arena self,
478 message: &mut Message,
479 arena_ctx: &mut ArenaContext<'arena>,
480 engine: &Arc<Engine>,
481 mapping_snapshots: Option<&mut Vec<Value>>,
482 ) -> Option<Result<(TaskOutcome, Vec<Change>)>> {
483 match self {
484 FunctionConfig::Map { input, .. } => {
485 Some(input.execute_in_arena(message, arena_ctx, engine, mapping_snapshots))
486 }
487 FunctionConfig::Validation { input, .. } => {
488 Some(input.execute_in_arena(message, arena_ctx, engine))
489 }
490 FunctionConfig::ParseJson { input, .. } => {
491 Some(execute_parse_json_in_arena(message, input, arena_ctx))
492 }
493 FunctionConfig::ParseXml { input, .. } => {
494 let result = execute_parse_xml(message, input);
500 Some(refresh_data_on_success(message, arena_ctx, result))
501 }
502 FunctionConfig::PublishJson { input, .. } => {
503 let result = execute_publish_json(message, input);
504 Some(refresh_data_on_success(message, arena_ctx, result))
505 }
506 FunctionConfig::PublishXml { input, .. } => {
507 let result = execute_publish_xml(message, input);
508 Some(refresh_data_on_success(message, arena_ctx, result))
509 }
510 FunctionConfig::Filter { input, .. } => {
511 Some(input.execute_in_arena(message, arena_ctx, engine))
512 }
513 FunctionConfig::Log { input, .. } => {
514 Some(input.execute_in_arena(message, arena_ctx, engine))
515 }
516 FunctionConfig::HttpCall { .. }
517 | FunctionConfig::Enrich { .. }
518 | FunctionConfig::PublishKafka { .. }
519 | FunctionConfig::Custom { .. } => None,
520 }
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use serde_json::json;
528
529 fn parse(value: serde_json::Value) -> std::result::Result<FunctionConfig, serde_json::Error> {
530 serde_json::from_value(value)
531 }
532
533 #[test]
534 fn map_with_valid_config_deserializes_to_map_variant() {
535 let cfg = parse(json!({
536 "name": "map",
537 "input": {
538 "mappings": [
539 { "path": "data.x", "logic": { "var": "data.y" } }
540 ]
541 }
542 }))
543 .expect("valid map config should deserialize");
544 assert!(matches!(cfg, FunctionConfig::Map { .. }));
545 }
546
547 #[test]
548 fn map_with_missing_mappings_gives_clear_error() {
549 let err = parse(json!({
550 "name": "map",
551 "input": {}
552 }))
553 .expect_err("map with empty input should fail");
554 let msg = err.to_string();
555 assert!(
556 msg.starts_with("config for function 'map':"),
557 "error should be prefixed with function envelope, got: {msg}"
558 );
559 assert!(
560 msg.contains("mappings"),
561 "error should mention the missing field, got: {msg}"
562 );
563 }
564
565 #[test]
566 fn map_with_wrong_input_shape_gives_clear_error() {
567 let err = parse(json!({
568 "name": "map",
569 "input": { "mappings": "not an array" }
570 }))
571 .expect_err("map with bad mappings type should fail");
572 let msg = err.to_string();
573 assert!(
574 msg.starts_with("config for function 'map':"),
575 "error should be prefixed with function envelope, got: {msg}"
576 );
577 }
578
579 #[test]
580 fn validation_accepts_both_spellings() {
581 for name in ["validate", "validation"] {
582 let cfg = parse(json!({
583 "name": name,
584 "input": { "rules": [] }
585 }))
586 .unwrap_or_else(|e| panic!("'{name}' should deserialize: {e}"));
587 assert!(matches!(cfg, FunctionConfig::Validation { .. }));
588 }
589 }
590
591 #[test]
592 fn unknown_name_falls_through_to_custom() {
593 let cfg = parse(json!({
594 "name": "my_custom_handler",
595 "input": { "anything": "goes" }
596 }))
597 .expect("unknown name should produce Custom");
598 match cfg {
599 FunctionConfig::Custom {
600 name,
601 compiled_input,
602 ..
603 } => {
604 assert_eq!(name, "my_custom_handler");
605 assert!(compiled_input.is_none());
606 }
607 other => panic!("expected Custom, got {other:?}"),
608 }
609 }
610
611 #[test]
612 fn missing_name_field_errors() {
613 let err = parse(json!({ "input": {} })).expect_err("missing name should fail");
614 assert!(err.to_string().contains("name"));
615 }
616
617 #[test]
618 fn missing_input_field_errors() {
619 let err = parse(json!({ "name": "map" })).expect_err("missing input should fail");
620 assert!(err.to_string().contains("input"));
621 }
622
623 #[test]
624 fn http_call_with_missing_connector_gives_clear_error() {
625 let err = parse(json!({
626 "name": "http_call",
627 "input": { "method": "GET" }
628 }))
629 .expect_err("http_call needs connector");
630 let msg = err.to_string();
631 assert!(
632 msg.starts_with("config for function 'http_call':"),
633 "error should be prefixed with function envelope, got: {msg}"
634 );
635 assert!(msg.contains("connector"));
636 }
637
638 #[test]
639 fn builtin_names_never_fall_through_to_custom() {
640 for name in BUILTIN_FUNCTION_NAMES {
644 let cfg = parse(json!({
645 "name": name,
646 "input": {}
647 }));
648 match cfg {
649 Ok(c) => assert!(
650 !matches!(c, FunctionConfig::Custom { .. }),
651 "name '{name}' silently fell through to Custom"
652 ),
653 Err(e) => assert!(
654 e.to_string()
655 .starts_with(&format!("config for function '{name}':")),
656 "name '{name}' failed without envelope: {e}"
657 ),
658 }
659
660 assert!(
663 builtin_function_kind(name).is_some(),
664 "name '{name}' is in BUILTIN_FUNCTION_NAMES but classifies as None"
665 );
666 }
667 }
668
669 fn parse_http_call(
671 input: serde_json::Value,
672 ) -> std::result::Result<HttpCallConfig, serde_json::Error> {
673 match parse(json!({ "name": "http_call", "input": input }))? {
674 FunctionConfig::HttpCall { input, .. } => Ok(input),
675 other => panic!("expected HttpCall, got {other:?}"),
676 }
677 }
678
679 #[test]
680 fn http_call_response_path_is_read_under_its_own_name() {
681 let cfg = parse_http_call(json!({ "connector": "c", "response_path": "data.x" }))
682 .expect("response_path should parse");
683 assert_eq!(cfg.response_path.as_deref(), Some("data.x"));
684 }
685
686 #[test]
687 fn http_call_response_path_accepts_the_output_alias() {
688 let cfg = parse_http_call(json!({ "connector": "c", "output": "data.x" }))
691 .expect("output should be accepted as an alias");
692 assert_eq!(cfg.response_path.as_deref(), Some("data.x"));
693 }
694
695 #[test]
696 fn http_call_response_path_is_optional() {
697 let cfg = parse_http_call(json!({ "connector": "c" })).expect("no destination is valid");
698 assert_eq!(cfg.response_path, None);
699 }
700
701 #[test]
702 fn http_call_rejects_both_destination_keys_in_either_order() {
703 for input in [
706 json!({ "connector": "c", "response_path": "a", "output": "b" }),
707 json!({ "connector": "c", "output": "b", "response_path": "a" }),
708 ] {
709 let err = parse_http_call(input.clone())
710 .expect_err("supplying both destination keys must fail");
711 let msg = err.to_string();
712 assert!(
713 msg.starts_with("config for function 'http_call':"),
714 "error should carry the function envelope, got: {msg}"
715 );
716 assert!(
717 msg.contains("duplicate field"),
718 "error should name the conflict, got: {msg}"
719 );
720 }
721 }
722
723 #[test]
724 fn http_call_rejects_a_misspelled_destination_field() {
725 for bad in ["outputs", "Output", "respose_path", "response-path"] {
729 let mut input = serde_json::Map::new();
730 input.insert("connector".to_string(), json!("c"));
731 input.insert(bad.to_string(), json!("data.x"));
732
733 let err = parse_http_call(serde_json::Value::Object(input))
734 .expect_err("a misspelled field must be rejected, not silently discarded");
735 let msg = err.to_string();
736 assert!(
737 msg.starts_with("config for function 'http_call':"),
738 "error should carry the function envelope, got: {msg}"
739 );
740 assert!(
741 msg.contains("unknown field"),
742 "error should say the field is unknown, got: {msg}"
743 );
744 assert!(
745 msg.contains(bad),
746 "error should name the offending field '{bad}', got: {msg}"
747 );
748 }
749 }
750
751 #[test]
752 fn enrich_does_not_accept_the_output_alias() {
753 let err = parse(json!({
757 "name": "enrich",
758 "input": { "connector": "c", "output": "data.x" }
759 }))
760 .expect_err("enrich has no `output` field");
761 let msg = err.to_string();
762 assert!(
763 msg.starts_with("config for function 'enrich':"),
764 "error should carry the function envelope, got: {msg}"
765 );
766
767 let ok = parse(json!({
769 "name": "enrich",
770 "input": { "connector": "c", "merge_path": "data.x" }
771 }))
772 .expect("merge_path is enrich's destination field");
773 assert!(matches!(ok, FunctionConfig::Enrich { .. }));
774 }
775
776 #[test]
777 fn publish_kafka_rejects_unknown_fields() {
778 let err = parse(json!({
779 "name": "publish_kafka",
780 "input": { "connector": "c", "topic": "t", "tpoic": "typo" }
781 }))
782 .expect_err("publish_kafka should reject an unknown field");
783 assert!(err.to_string().contains("unknown field"), "got: {err}");
784 }
785
786 #[test]
787 fn connector_is_returned_for_the_three_typed_integrations() {
788 let cases = [
789 (
790 json!({ "name": "http_call", "input": { "connector": "user_service" } }),
791 "user_service",
792 ),
793 (
794 json!({ "name": "enrich",
795 "input": { "connector": "ref_data", "merge_path": "data.out" } }),
796 "ref_data",
797 ),
798 (
799 json!({ "name": "publish_kafka",
800 "input": { "connector": "events", "topic": "t" } }),
801 "events",
802 ),
803 ];
804 for (input, expected) in cases {
805 let cfg = parse(input.clone()).expect("should parse");
806 assert_eq!(cfg.connector(), Some(expected), "for {input}");
807 }
808 }
809
810 #[test]
811 fn connector_is_none_for_every_non_connector_builtin() {
812 let minimal_input = |name: &str| -> serde_json::Value {
815 match name {
816 "map" => json!({ "mappings": [] }),
817 "validation" | "validate" => json!({ "rules": [] }),
818 "parse_json" | "parse_xml" | "publish_json" | "publish_xml" => {
819 json!({ "source": "data.in", "target": "out" })
820 }
821 "filter" => json!({ "condition": true }),
822 "log" => json!({ "message": "hi" }),
823 _ => json!({}),
824 }
825 };
826
827 for name in BUILTIN_FUNCTION_NAMES {
828 if matches!(*name, "http_call" | "enrich" | "publish_kafka") {
829 continue;
830 }
831 let cfg = parse(json!({ "name": name, "input": minimal_input(name) }))
832 .unwrap_or_else(|e| panic!("'{name}' should parse: {e}"));
833 assert_eq!(cfg.connector(), None, "'{name}' names no connector");
834 }
835 }
836
837 #[test]
838 fn connector_reads_the_custom_convention() {
839 let cfg = parse(json!({
840 "name": "pg_query",
841 "input": { "connector": "pg_main", "database": "orders" }
842 }))
843 .unwrap();
844 assert_eq!(cfg.connector(), Some("pg_main"));
845 }
846
847 #[test]
848 fn connector_is_none_for_a_custom_input_without_a_string_connector() {
849 for input in [
851 json!({}), json!({ "connector": 7 }), json!({ "connector": true }), json!({ "connector": null }), json!({ "connector": ["a"] }), json!({ "connector": { "n": "a" } }), json!([]), json!(7), ] {
860 let cfg = parse(json!({ "name": "my_handler", "input": input.clone() }))
861 .unwrap_or_else(|e| panic!("custom should parse {input}: {e}"));
862 assert_eq!(cfg.connector(), None, "for input {input}");
863 }
864 }
865
866 #[test]
867 fn connector_returns_an_empty_name_verbatim() {
868 let typed = parse(json!({ "name": "http_call", "input": { "connector": "" } })).unwrap();
872 assert_eq!(typed.connector(), Some(""));
873
874 let custom = parse(json!({ "name": "x", "input": { "connector": "" } })).unwrap();
875 assert_eq!(custom.connector(), Some(""));
876 }
877
878 #[test]
879 fn connector_returns_a_non_ascii_name_byte_for_byte() {
880 let cfg =
882 parse(json!({ "name": "http_call", "input": { "connector": "連携先" } })).unwrap();
883 assert_eq!(cfg.connector(), Some("連携先"));
884 }
885
886 #[test]
887 fn builtin_function_kind_is_none_for_non_builtins() {
888 for name in [
890 "",
891 "__not_a_builtin__",
892 "HTTP_CALL", "htttp_call", "map ", "publish_kafk", ] {
897 assert_eq!(
898 builtin_function_kind(name),
899 None,
900 "'{name}' must not classify as a built-in"
901 );
902 assert!(!is_builtin_function(name));
903 }
904 }
905
906 #[test]
907 fn builtin_kinds_partition_matches_real_dispatch_behaviour() {
908 let minimal_input = |name: &str| -> serde_json::Value {
915 match name {
916 "map" => json!({ "mappings": [] }),
917 "validation" | "validate" => json!({ "rules": [] }),
918 "parse_json" | "parse_xml" | "publish_json" | "publish_xml" => {
919 json!({ "source": "data.in", "target": "out" })
920 }
921 "filter" => json!({ "condition": true }),
922 "log" => json!({ "message": "hi" }),
923 "http_call" => json!({ "connector": "c" }),
924 "enrich" => json!({ "connector": "c", "merge_path": "data.out" }),
925 "publish_kafka" => json!({ "connector": "c", "topic": "t" }),
926 _ => json!({}),
929 }
930 };
931
932 for name in BUILTIN_FUNCTION_NAMES {
933 let kind = builtin_function_kind(name)
934 .unwrap_or_else(|| panic!("'{name}' must classify as a built-in"));
935 let cfg = parse(json!({ "name": name, "input": minimal_input(name) }))
936 .unwrap_or_else(|e| panic!("'{name}' should parse with minimal input: {e}"));
937
938 assert_eq!(
939 cfg.is_sync_builtin(),
940 matches!(kind, BuiltinKind::SelfContained),
941 "'{name}' classifies as {kind:?} but is_sync_builtin() is {}",
942 cfg.is_sync_builtin()
943 );
944 }
945 }
946
947 #[test]
948 fn requires_handler_kind_covers_exactly_the_config_only_integrations() {
949 for name in ["http_call", "enrich", "publish_kafka"] {
951 assert_eq!(
952 builtin_function_kind(name),
953 Some(BuiltinKind::RequiresHandler),
954 "'{name}' ships as config only and needs a registered handler"
955 );
956 }
957
958 for name in [
962 "map",
963 "validation",
964 "validate",
965 "parse_json",
966 "parse_xml",
967 "publish_json",
968 "publish_xml",
969 "filter",
970 "log",
971 ] {
972 assert_eq!(
973 builtin_function_kind(name),
974 Some(BuiltinKind::SelfContained),
975 "'{name}' is executed by this crate"
976 );
977 }
978 }
979}