Skip to main content

busbar_sf_agentscript/
validation.rs

1use crate::ast::{
2    ActionDef, AgentFile, ConnectionEntry, Expr, LanguageEntry, Type, VariableDecl, VariableKind,
3};
4use serde::Serialize;
5use std::ops::Range;
6
7#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
8pub enum Severity {
9    Error,
10    Warning,
11}
12
13#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
14pub struct SemanticError {
15    pub message: String,
16    pub span: Option<Range<usize>>,
17    pub severity: Severity,
18    pub hint: Option<String>,
19}
20
21pub fn validate_ast(ast: &AgentFile) -> Vec<SemanticError> {
22    let mut errors = Vec::new();
23
24    // Rule 1 & 2: Variables
25    if let Some(vars_block) = &ast.variables {
26        for var in &vars_block.node.variables {
27            validate_variable(&var.node, &mut errors);
28        }
29    }
30
31    // Rule 3: Locale Code Validation
32    if let Some(lang_block) = &ast.language {
33        for entry in &lang_block.node.entries {
34            validate_language_entry(&entry.node, &mut errors);
35        }
36    }
37
38    // Rule 4: Outbound Route Type Validation
39    for conn_block in &ast.connections {
40        for entry in &conn_block.node.entries {
41            validate_connection_entry(&entry.node, &mut errors);
42        }
43    }
44
45    // Rule 5: Action Input Keyword Collision
46    // Actions can be in start_agent and topics
47    if let Some(start_agent) = &ast.start_agent {
48        if let Some(actions) = &start_agent.node.actions {
49            for action in &actions.node.actions {
50                validate_action_def(&action.node, &mut errors);
51            }
52        }
53    }
54
55    for topic in &ast.topics {
56        if let Some(actions) = &topic.node.actions {
57            for action in &actions.node.actions {
58                validate_action_def(&action.node, &mut errors);
59            }
60        }
61    }
62
63    errors
64}
65
66fn validate_variable(var: &VariableDecl, errors: &mut Vec<SemanticError>) {
67    // Rule 1: Mutable Variable Type Restrictions
68    if let VariableKind::Mutable = var.kind {
69        match var.ty.node {
70            Type::Integer | Type::Long | Type::Datetime | Type::Time => {
71                errors.push(SemanticError {
72                    message: format!(
73                        "Variable '{}' with type {:?} is not supported for mutable variables. This may be supported in the future.",
74                        var.name.node, var.ty.node
75                    ),
76                    span: Some(var.ty.span.clone()),
77                    severity: Severity::Error,
78                    hint: Some("Allowed mutable types: String, Boolean, Number, Currency, Date, Id, Object, Timestamp".to_string()),
79                });
80            }
81            _ => {}
82        }
83    }
84
85    // Rule 2: Context Variable Object Type
86    // Linked variables (source starts with @context.) cannot be Object type.
87    if let VariableKind::Linked = var.kind {
88        if let Some(source) = &var.source {
89            if source.node.namespace == "context" {
90                if let Type::Object = var.ty.node {
91                    errors.push(SemanticError {
92                        message: format!(
93                            "Context variable '{}' cannot be an object type",
94                            var.name.node
95                        ),
96                        span: Some(var.ty.span.clone()),
97                        severity: Severity::Error,
98                        hint: None,
99                    });
100                }
101            }
102        }
103    }
104}
105
106fn validate_language_entry(entry: &LanguageEntry, errors: &mut Vec<SemanticError>) {
107    // Rule 3: Locale Code Validation
108    if entry.name.node == "additional_locales" {
109        if let Expr::String(ref s) = entry.value.node {
110            let valid_locales = [
111                "ar", "bg", "ca", "cs", "da", "de", "el", "en_AU", "en_GB", "en_US", "es", "es_MX",
112                "et", "fi", "fr", "fr_CA", "hi", "hr", "hu", "in", "it", "iw", "ja", "ko", "nl_NL",
113                "no", "pl", "pt_BR", "pt_PT", "ro", "sv", "th", "tl", "tr", "vi", "zh_CN", "zh_TW",
114            ];
115
116            let codes: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
117            for code in codes {
118                if !valid_locales.contains(&code) {
119                    errors.push(SemanticError {
120                        message: format!("Invalid additional_locale '{}'.", code),
121                        span: Some(entry.value.span.clone()),
122                        severity: Severity::Error,
123                        hint: Some(format!("Valid locales are: {}", valid_locales.join(", "))),
124                    });
125                }
126            }
127        }
128    }
129}
130
131fn validate_connection_entry(entry: &ConnectionEntry, errors: &mut Vec<SemanticError>) {
132    // Rule 4: Outbound Route Type Validation
133    if entry.name.node == "outbound_route_type" && entry.value.node != "OmniChannelFlow" {
134        errors.push(SemanticError {
135            message: format!(
136                "invalid outbound_route_type, found '{}' expected 'OmniChannelFlow'",
137                entry.value.node
138            ),
139            span: Some(entry.value.span.clone()),
140            severity: Severity::Error,
141            hint: None,
142        });
143    }
144}
145
146fn validate_action_def(action: &ActionDef, errors: &mut Vec<SemanticError>) {
147    // Rule 5: Action Input Keyword Collision
148    if let Some(inputs) = &action.inputs {
149        for param in &inputs.node {
150            let name = &param.node.name.node;
151            match name.as_str() {
152                "description" | "label" | "target" | "inputs" | "outputs" => {
153                    errors.push(SemanticError {
154                        message: format!(
155                            "Action input parameter '{}' collides with keyword '{}' and may cause platform parse errors",
156                            name, name
157                        ),
158                        span: Some(param.node.name.span.clone()),
159                        severity: Severity::Warning,
160                        hint: None,
161                    });
162                }
163                _ => {}
164            }
165        }
166    }
167}