Skip to main content

ai_agents_process/
config.rs

1//! Process configuration types for input/output transformation
2
3use 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}