1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7#[serde(deny_unknown_fields)]
8pub struct ProcessConfig {
9 #[serde(default)]
10 pub input: Vec<ProcessStage>,
11 #[serde(default)]
12 pub output: Vec<ProcessStage>,
13 #[serde(default)]
14 pub settings: ProcessSettings,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
19pub enum ProcessStage {
20 Normalize(NormalizeStage),
21 Detect(DetectStage),
22 Extract(ExtractStage),
23 Sanitize(SanitizeStage),
24 Transform(TransformStage),
25 Validate(ValidateStage),
26 Format(FormatStage),
27 Enrich(EnrichStage),
28 Conditional(ConditionalStage),
29}
30
31impl ProcessStage {
32 pub fn condition(&self) -> Option<&ConditionExpr> {
33 match self {
34 ProcessStage::Normalize(s) => s.condition.as_ref(),
35 ProcessStage::Detect(s) => s.condition.as_ref(),
36 ProcessStage::Extract(s) => s.condition.as_ref(),
37 ProcessStage::Sanitize(s) => s.condition.as_ref(),
38 ProcessStage::Transform(s) => s.condition.as_ref(),
39 ProcessStage::Validate(s) => s.condition.as_ref(),
40 ProcessStage::Format(s) => s.condition.as_ref(),
41 ProcessStage::Enrich(s) => s.condition.as_ref(),
42 ProcessStage::Conditional(_) => None,
43 }
44 }
45
46 pub fn id(&self) -> Option<&str> {
47 match self {
48 ProcessStage::Normalize(s) => s.id.as_deref(),
49 ProcessStage::Detect(s) => s.id.as_deref(),
50 ProcessStage::Extract(s) => s.id.as_deref(),
51 ProcessStage::Sanitize(s) => s.id.as_deref(),
52 ProcessStage::Transform(s) => s.id.as_deref(),
53 ProcessStage::Validate(s) => s.id.as_deref(),
54 ProcessStage::Format(s) => s.id.as_deref(),
55 ProcessStage::Enrich(s) => s.id.as_deref(),
56 ProcessStage::Conditional(s) => s.id.as_deref(),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, Default)]
62#[serde(deny_unknown_fields)]
63pub struct NormalizeStage {
64 #[serde(default)]
65 pub id: Option<String>,
66 #[serde(default)]
67 pub condition: Option<ConditionExpr>,
68 #[serde(default)]
69 pub config: NormalizeConfig,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct NormalizeConfig {
75 #[serde(default = "default_true")]
76 pub trim: bool,
77 #[serde(default)]
78 pub unicode: Option<UnicodeNormalization>,
79 #[serde(default)]
80 pub collapse_whitespace: bool,
81 #[serde(default)]
82 pub lowercase: bool,
83}
84
85impl Default for NormalizeConfig {
86 fn default() -> Self {
87 Self {
88 trim: true,
89 unicode: None,
90 collapse_whitespace: false,
91 lowercase: false,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "lowercase")]
98pub enum UnicodeNormalization {
99 Nfc,
100 Nfd,
101 Nfkc,
102 Nfkd,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106#[serde(deny_unknown_fields)]
107pub struct DetectStage {
108 #[serde(default)]
109 pub id: Option<String>,
110 #[serde(default)]
111 pub condition: Option<ConditionExpr>,
112 #[serde(default)]
113 pub config: DetectConfig,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, Default)]
117#[serde(deny_unknown_fields)]
118pub struct DetectConfig {
119 #[serde(default)]
120 pub llm: Option<String>,
121 #[serde(default)]
122 pub detect: Vec<DetectionType>,
123 #[serde(default)]
124 pub intents: Vec<IntentDefinition>,
125 #[serde(default)]
126 pub store_in_context: HashMap<String, String>,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
130#[serde(rename_all = "snake_case")]
131pub enum DetectionType {
132 Language,
133 Sentiment,
134 Intent,
135 Topic,
136 Formality,
137 Urgency,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub struct IntentDefinition {
143 pub id: String,
144 pub description: String,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, Default)]
148#[serde(deny_unknown_fields)]
149pub struct ExtractStage {
150 #[serde(default)]
151 pub id: Option<String>,
152 #[serde(default)]
153 pub condition: Option<ConditionExpr>,
154 #[serde(default)]
155 pub config: ExtractConfig,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, Default)]
159#[serde(deny_unknown_fields)]
160pub struct ExtractConfig {
161 #[serde(default)]
162 pub llm: Option<String>,
163 #[serde(default)]
164 pub schema: HashMap<String, FieldSchema>,
165 #[serde(default)]
166 pub store_in_context: Option<String>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, Default)]
170#[serde(deny_unknown_fields)]
171pub struct FieldSchema {
172 #[serde(rename = "type", default)]
173 pub field_type: FieldType,
174 #[serde(default)]
175 pub description: Option<String>,
176 #[serde(default)]
177 pub required: bool,
178 #[serde(default)]
179 pub values: Vec<String>,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
183#[serde(rename_all = "snake_case")]
184pub enum FieldType {
185 #[default]
186 String,
187 Number,
188 Integer,
189 Boolean,
190 Date,
191 Enum,
192 Array,
193 Object,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, Default)]
197#[serde(deny_unknown_fields)]
198pub struct SanitizeStage {
199 #[serde(default)]
200 pub id: Option<String>,
201 #[serde(default)]
202 pub condition: Option<ConditionExpr>,
203 #[serde(default)]
204 pub config: SanitizeConfig,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, Default)]
208#[serde(deny_unknown_fields)]
209pub struct SanitizeConfig {
210 #[serde(default)]
211 pub llm: Option<String>,
212 #[serde(default)]
213 pub pii: Option<PIISanitizeConfig>,
214 #[serde(default)]
215 pub harmful: Option<HarmfulContentConfig>,
216 #[serde(default)]
217 pub remove: Vec<String>,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(deny_unknown_fields)]
222pub struct PIISanitizeConfig {
223 #[serde(default)]
224 pub action: PIIAction,
225 #[serde(default)]
226 pub types: Vec<PIIType>,
227 #[serde(default = "default_mask_char")]
228 pub mask_char: String,
229}
230
231impl Default for PIISanitizeConfig {
232 fn default() -> Self {
233 Self {
234 action: PIIAction::Mask,
235 types: Vec::new(),
236 mask_char: default_mask_char(),
237 }
238 }
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
242#[serde(rename_all = "snake_case")]
243pub enum PIIAction {
244 #[default]
245 Mask,
246 Remove,
247 Flag,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
251#[serde(rename_all = "snake_case")]
252pub enum PIIType {
253 Email,
254 Phone,
255 CreditCard,
256 Ssn,
257 IpAddress,
258 Name,
259 Address,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, Default)]
263#[serde(deny_unknown_fields)]
264pub struct HarmfulContentConfig {
265 #[serde(default)]
266 pub detect: Vec<HarmfulContentType>,
267 #[serde(default)]
268 pub action: HarmfulAction,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(rename_all = "snake_case")]
273pub enum HarmfulContentType {
274 HateSpeech,
275 Violence,
276 SexualContent,
277 Harassment,
278 SelfHarm,
279 IllegalActivity,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
283#[serde(rename_all = "snake_case")]
284pub enum HarmfulAction {
285 #[default]
286 Flag,
287 Block,
288 Remove,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, Default)]
292#[serde(deny_unknown_fields)]
293pub struct TransformStage {
294 #[serde(default)]
295 pub id: Option<String>,
296 #[serde(default)]
297 pub condition: Option<ConditionExpr>,
298 #[serde(default)]
299 pub config: TransformConfig,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, Default)]
303#[serde(deny_unknown_fields)]
304pub struct TransformConfig {
305 #[serde(default)]
306 pub llm: Option<String>,
307 #[serde(default)]
308 pub prompt: Option<String>,
309 #[serde(default)]
310 pub max_output_tokens: Option<u32>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize, Default)]
314#[serde(deny_unknown_fields)]
315pub struct ValidateStage {
316 #[serde(default)]
317 pub id: Option<String>,
318 #[serde(default)]
319 pub condition: Option<ConditionExpr>,
320 #[serde(default)]
321 pub config: ValidateConfig,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, Default)]
325#[serde(deny_unknown_fields)]
326pub struct ValidateConfig {
327 #[serde(default)]
328 pub rules: Vec<ValidationRule>,
329 #[serde(default)]
330 pub llm: Option<String>,
331 #[serde(default)]
332 pub criteria: Vec<String>,
333 #[serde(default = "default_threshold")]
334 pub threshold: f32,
335 #[serde(default)]
336 pub on_fail: ValidationFailAction,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
340#[serde(untagged, deny_unknown_fields)]
341pub enum ValidationRule {
342 MinLength {
343 min_length: usize,
344 #[serde(default)]
345 on_fail: ValidationAction,
346 },
347 MaxLength {
348 max_length: usize,
349 #[serde(default)]
350 on_fail: ValidationAction,
351 },
352 Pattern {
353 pattern: String,
354 #[serde(default)]
355 on_fail: ValidationAction,
356 },
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, Default)]
360#[serde(deny_unknown_fields)]
361pub struct ValidationAction {
362 #[serde(default)]
363 pub action: ValidationActionType,
364 #[serde(default)]
365 pub message: Option<HashMap<String, String>>,
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
369#[serde(rename_all = "snake_case")]
370pub enum ValidationActionType {
371 #[default]
372 Reject,
373 Truncate,
374 Warn,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, Default)]
378#[serde(deny_unknown_fields)]
379pub struct ValidationFailAction {
380 #[serde(default)]
381 pub action: ValidationFailType,
382 #[serde(default)]
383 pub max_retries: Option<u32>,
384 #[serde(default)]
385 pub feedback_to_agent: bool,
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
389#[serde(rename_all = "snake_case")]
390pub enum ValidationFailType {
391 #[default]
392 Reject,
393 Regenerate,
394 Warn,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, Default)]
398#[serde(deny_unknown_fields)]
399pub struct FormatStage {
400 #[serde(default)]
401 pub id: Option<String>,
402 #[serde(default)]
403 pub condition: Option<ConditionExpr>,
404 #[serde(default)]
405 pub config: FormatConfig,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize, Default)]
409#[serde(deny_unknown_fields)]
410pub struct FormatConfig {
411 #[serde(default)]
412 pub template: Option<String>,
413 #[serde(default)]
414 pub channels: HashMap<String, ChannelFormat>,
415 #[serde(default)]
416 pub channel: Option<String>,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize, Default)]
420#[serde(deny_unknown_fields)]
421pub struct ChannelFormat {
422 #[serde(default)]
423 pub template: Option<String>,
424 #[serde(default)]
425 pub format: Option<OutputFormat>,
426 #[serde(default)]
427 pub max_length: Option<usize>,
428 #[serde(default)]
429 pub markdown: bool,
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
433#[serde(rename_all = "snake_case")]
434pub enum OutputFormat {
435 #[default]
436 Text,
437 Html,
438 Json,
439 Markdown,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize, Default)]
443#[serde(deny_unknown_fields)]
444pub struct EnrichStage {
445 #[serde(default)]
446 pub id: Option<String>,
447 #[serde(default)]
448 pub condition: Option<ConditionExpr>,
449 #[serde(default)]
450 pub config: EnrichConfig,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize, Default)]
454#[serde(deny_unknown_fields)]
455pub struct EnrichConfig {
456 #[serde(default)]
457 pub source: EnrichSource,
458 #[serde(default)]
459 pub store_in_context: Option<String>,
460 #[serde(default)]
461 pub on_error: EnrichErrorAction,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, Default)]
465#[serde(tag = "source", rename_all = "snake_case", deny_unknown_fields)]
466pub enum EnrichSource {
467 #[default]
468 None,
469 Api {
470 url: String,
471 #[serde(default = "default_method")]
472 method: String,
473 #[serde(default)]
474 headers: HashMap<String, String>,
475 #[serde(default)]
476 body: Option<serde_json::Value>,
477 #[serde(default)]
478 extract: HashMap<String, String>,
479 },
480 File {
481 path: String,
482 #[serde(default)]
483 format: Option<String>,
484 },
485 Tool {
486 tool: String,
487 #[serde(default)]
488 args: serde_json::Value,
489 },
490}
491
492#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
493#[serde(rename_all = "snake_case")]
494pub enum EnrichErrorAction {
495 #[default]
496 Continue,
497 Stop,
498 Warn,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize, Default)]
502#[serde(deny_unknown_fields)]
503pub struct ConditionalStage {
504 #[serde(default)]
505 pub id: Option<String>,
506 #[serde(default)]
507 pub config: ConditionalConfig,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize, Default)]
511#[serde(deny_unknown_fields)]
512pub struct ConditionalConfig {
513 #[serde(default)]
514 pub condition: Option<ConditionExpr>,
515 #[serde(default, rename = "then")]
516 pub then_stages: Vec<ProcessStage>,
517 #[serde(default, rename = "else")]
518 pub else_stages: Vec<ProcessStage>,
519}
520
521#[derive(Debug, Clone, Serialize)]
522#[serde(untagged)]
523pub enum ConditionExpr {
524 All { all: Vec<ConditionExpr> },
525 Any { any: Vec<ConditionExpr> },
526 Simple(HashMap<String, serde_json::Value>),
527}
528
529#[derive(Deserialize)]
530#[serde(deny_unknown_fields)]
531struct AllConditionExpr {
532 all: Vec<ConditionExpr>,
533}
534
535#[derive(Deserialize)]
536#[serde(deny_unknown_fields)]
537struct AnyConditionExpr {
538 any: Vec<ConditionExpr>,
539}
540
541impl<'de> Deserialize<'de> for ConditionExpr {
542 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
543 where
544 D: Deserializer<'de>,
545 {
546 let value = serde_json::Value::deserialize(deserializer)?;
547 let fields = value
548 .as_object()
549 .ok_or_else(|| D::Error::custom("condition must be a map"))?;
550
551 if fields.contains_key("all") {
552 let condition =
553 serde_json::from_value::<AllConditionExpr>(value).map_err(D::Error::custom)?;
554 return Ok(Self::All { all: condition.all });
555 }
556
557 if fields.contains_key("any") {
558 let condition =
559 serde_json::from_value::<AnyConditionExpr>(value).map_err(D::Error::custom)?;
560 return Ok(Self::Any { any: condition.any });
561 }
562
563 Ok(Self::Simple(fields.clone().into_iter().collect()))
564 }
565}
566
567impl Default for ConditionExpr {
568 fn default() -> Self {
569 ConditionExpr::Simple(HashMap::new())
570 }
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize)]
574#[serde(deny_unknown_fields)]
575pub struct ProcessSettings {
576 #[serde(default)]
577 pub on_stage_error: StageErrorConfig,
578 #[serde(default = "default_timeout")]
579 pub timeout_ms: u64,
580 #[serde(default)]
581 pub cache: ProcessCacheConfig,
582 #[serde(default)]
583 pub debug: ProcessDebugConfig,
584}
585
586impl Default for ProcessSettings {
587 fn default() -> Self {
588 Self {
589 on_stage_error: StageErrorConfig::default(),
590 timeout_ms: default_timeout(),
591 cache: ProcessCacheConfig::default(),
592 debug: ProcessDebugConfig::default(),
593 }
594 }
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, Default)]
598#[serde(deny_unknown_fields)]
599pub struct StageErrorConfig {
600 #[serde(default)]
601 pub default: StageErrorAction,
602 #[serde(default)]
603 pub retry: Option<StageRetryConfig>,
604}
605
606#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
607#[serde(rename_all = "snake_case")]
608pub enum StageErrorAction {
609 #[default]
610 Continue,
611 Stop,
612 Retry,
613}
614
615#[derive(Debug, Clone, Serialize, Deserialize)]
616#[serde(deny_unknown_fields)]
617pub struct StageRetryConfig {
618 #[serde(default = "default_retry")]
619 pub max_retries: u32,
620 #[serde(default = "default_backoff")]
621 pub backoff_ms: u64,
622}
623
624impl Default for StageRetryConfig {
625 fn default() -> Self {
626 Self {
627 max_retries: default_retry(),
628 backoff_ms: default_backoff(),
629 }
630 }
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize, Default)]
634#[serde(deny_unknown_fields)]
635pub struct ProcessCacheConfig {
636 #[serde(default)]
637 pub enabled: bool,
638 #[serde(default)]
639 pub stages: Vec<String>,
640 #[serde(default = "default_cache_ttl")]
641 pub ttl_seconds: u64,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize, Default)]
645#[serde(deny_unknown_fields)]
646pub struct ProcessDebugConfig {
647 #[serde(default)]
648 pub log_stages: bool,
649 #[serde(default)]
650 pub include_timing: bool,
651}
652
653fn default_true() -> bool {
654 true
655}
656
657fn default_mask_char() -> String {
658 "*".to_string()
659}
660
661fn default_threshold() -> f32 {
662 0.7
663}
664
665fn default_timeout() -> u64 {
666 5000
667}
668
669fn default_retry() -> u32 {
670 2
671}
672
673fn default_backoff() -> u64 {
674 100
675}
676
677fn default_cache_ttl() -> u64 {
678 300
679}
680
681fn default_method() -> String {
682 "GET".to_string()
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688
689 #[test]
690 fn test_default_config() {
691 let config = ProcessConfig::default();
692 assert!(config.input.is_empty());
693 assert!(config.output.is_empty());
694 assert_eq!(config.settings.timeout_ms, 5000);
695 }
696
697 #[test]
698 fn test_yaml_parsing() {
699 let yaml = r#"
700input:
701 - type: normalize
702 id: basic_normalize
703 config:
704 trim: true
705 collapse_whitespace: true
706 - type: detect
707 id: detect_language
708 config:
709 llm: fast
710 detect:
711 - language
712 - sentiment
713 intents:
714 - id: greeting
715 description: "User is saying hello"
716 - type: extract
717 config:
718 llm: fast
719 schema:
720 user_name:
721 type: string
722 description: "User's name if mentioned"
723output:
724 - type: validate
725 config:
726 llm: fast
727 criteria:
728 - "Response is helpful"
729 threshold: 0.8
730settings:
731 timeout_ms: 3000
732"#;
733 let config: ProcessConfig = serde_yaml::from_str(yaml).unwrap();
734 assert_eq!(config.input.len(), 3);
735 assert_eq!(config.output.len(), 1);
736 assert_eq!(config.settings.timeout_ms, 3000);
737 }
738
739 #[test]
740 fn test_yaml_rejects_unknown_process_field() {
741 let yaml = r#"
742input: []
743unexpected: true
744"#;
745
746 assert!(serde_yaml::from_str::<ProcessConfig>(yaml).is_err());
747 }
748
749 #[test]
750 fn test_yaml_rejects_unknown_stage_field() {
751 let yaml = r#"
752input:
753 - type: normalize
754 config: {}
755 unexpected: true
756"#;
757
758 assert!(serde_yaml::from_str::<ProcessConfig>(yaml).is_err());
759 }
760
761 #[test]
762 fn test_yaml_rejects_unknown_config_field() {
763 let yaml = r#"
764input:
765 - type: normalize
766 config:
767 unexpected: true
768"#;
769
770 assert!(serde_yaml::from_str::<ProcessConfig>(yaml).is_err());
771 }
772
773 #[test]
774 fn test_yaml_rejects_unknown_nested_config_field() {
775 let yaml = r#"
776input:
777 - type: sanitize
778 config:
779 pii:
780 action: mask
781 unexpected: true
782"#;
783
784 assert!(serde_yaml::from_str::<ProcessConfig>(yaml).is_err());
785 }
786
787 #[test]
788 fn test_yaml_rejects_unknown_dynamic_map_value_field() {
789 let yaml = r#"
790input:
791 - type: extract
792 config:
793 schema:
794 dynamic_field_name:
795 type: string
796 unexpected: true
797"#;
798
799 assert!(serde_yaml::from_str::<ProcessConfig>(yaml).is_err());
800 }
801
802 #[test]
803 fn test_yaml_rejects_unknown_condition_group_field() {
804 let yaml = r#"
805input:
806 - type: normalize
807 condition:
808 all: []
809 unexpected: true
810"#;
811
812 assert!(serde_yaml::from_str::<ProcessConfig>(yaml).is_err());
813 }
814
815 #[test]
816 fn test_normalize_config() {
817 let config = NormalizeConfig::default();
818 assert!(config.trim);
819 assert!(!config.collapse_whitespace);
820 }
821
822 #[test]
823 fn test_field_type() {
824 let yaml = r#"
825type: enum
826values:
827 - low
828 - medium
829 - high
830description: "Priority level"
831"#;
832 let schema: FieldSchema = serde_yaml::from_str(yaml).unwrap();
833 assert_eq!(schema.field_type, FieldType::Enum);
834 assert_eq!(schema.values.len(), 3);
835 }
836
837 #[test]
838 fn test_condition_parsing_simple() {
839 let yaml = r#"
840type: extract
841condition:
842 context.session.user_name:
843 exists: false
844config:
845 schema:
846 user_name:
847 type: string
848"#;
849 let stage: ProcessStage = serde_yaml::from_str(yaml).unwrap();
850 assert!(stage.condition().is_some());
851 }
852
853 #[test]
854 fn test_condition_parsing_all() {
855 let yaml = r#"
856type: enrich
857condition:
858 all:
859 - context.input.extracted.user_name:
860 exists: true
861 - context.session.user_profile:
862 exists: false
863config: {}
864"#;
865 let stage: ProcessStage = serde_yaml::from_str(yaml).unwrap();
866 match stage.condition().unwrap() {
867 ConditionExpr::All { all } => assert_eq!(all.len(), 2),
868 _ => panic!("Expected All condition"),
869 }
870 }
871
872 #[test]
873 fn test_condition_parsing_any() {
874 let yaml = r#"
875type: detect
876condition:
877 any:
878 - context.session.language:
879 exists: false
880 - context.session.force_detect: true
881config:
882 detect:
883 - language
884"#;
885 let stage: ProcessStage = serde_yaml::from_str(yaml).unwrap();
886 match stage.condition().unwrap() {
887 ConditionExpr::Any { any } => assert_eq!(any.len(), 2),
888 _ => panic!("Expected Any condition"),
889 }
890 }
891
892 #[test]
893 fn test_process_stage_condition_accessor() {
894 let stage = ProcessStage::Extract(ExtractStage {
895 id: Some("test".to_string()),
896 condition: Some(ConditionExpr::Simple(HashMap::new())),
897 config: ExtractConfig::default(),
898 });
899 assert!(stage.condition().is_some());
900 assert_eq!(stage.id(), Some("test"));
901 }
902
903 #[test]
904 fn test_process_stage_no_condition() {
905 let stage = ProcessStage::Normalize(NormalizeStage::default());
906 assert!(stage.condition().is_none());
907 }
908}