Skip to main content

dw_parser/
pel.rs

1// Copyright (c) 2026, Salesforce, Inc.,
2// All rights reserved.
3// For full license text, see the LICENSE.txt file
4
5//! PEL Parser Library
6//!
7//! This module provides the function and types to parse a DataWeave expression into PEL.
8//!
9//! ## Primary types
10//!
11//! - [`InputContext`]: Input context containing inputs (e.g. payload, attributes) and DW functions (e.g. substringBefore, fromBase64) for compiling the expression.
12//! - [`CompilationResult`]: Contains the compiled PEL expression if compile was successful or error messages upon failure.
13
14use crate::dw::{
15    parse_dw, unescape_string, AstNode, ComparisonType, ImportedElement, Location, LogicalType,
16    Message, MessageKind,
17};
18use crate::json::escape_str;
19
20/// Sets the context to parse the DataWeave expression. It receives:
21/// * inputs that will be used (e.g. payload)
22/// * functions that are part of the expression context (e.g. fromBase64)
23#[derive(Clone, PartialEq, Debug, Default)]
24pub struct InputContext<'a> {
25    pub inputs: Vec<&'a str>,
26    pub functions: Vec<&'a str>,
27}
28
29#[derive(Clone, PartialEq, Debug, Default)]
30struct DeclaredImports {
31    pub imports: Vec<ImportDefinition>,
32}
33
34#[derive(Clone, PartialEq, Debug, Default)]
35struct ImportDefinition {
36    module: String,
37    alias: Option<String>,
38    imported_elements: Vec<ImportElement>,
39}
40
41#[derive(Clone, PartialEq, Debug, Default)]
42struct ImportElement {
43    element: String,
44    alias: Option<String>,
45}
46
47/// Contains the actual compilation result:
48/// * pel: expression as a String
49/// * messages: vector of [Message] containing any potential error messages.
50#[derive(Clone, PartialEq, Debug)]
51pub struct CompilationResult {
52    pub pel: Option<String>,
53    pub messages: Vec<Message>,
54}
55
56#[derive(Clone, PartialEq, Debug, Default)]
57pub struct DeclaredNamespaces {
58    pub namespaces: Vec<NamespaceInformation>,
59}
60
61impl DeclaredNamespaces {
62    pub fn contains_prefix(&self, prefix: &String) -> bool {
63        self.namespaces.iter().any(|n| n.prefix.eq(prefix))
64    }
65
66    pub fn resolve_prefix(&self, prefix: &String) -> Option<&String> {
67        self.namespaces
68            .iter()
69            .find(|n| n.prefix.eq(prefix))
70            .map(|ns| &ns.uri)
71    }
72}
73
74#[derive(Clone, PartialEq, Debug, Default)]
75pub struct NamespaceInformation {
76    pub prefix: String,
77    pub uri: String,
78}
79
80fn is_imported(var_name: &str, ctx: &InputContext, imports: &DeclaredImports) -> bool {
81    //Check dw::Core as it is allways imported
82    let mut fqn: String = String::new();
83    fqn.push_str("dw::Core");
84    fqn.push_str("::");
85    fqn.push_str(var_name);
86    if ctx.functions.contains(&fqn.as_str()) {
87        return true;
88    }
89
90    for import in &imports.imports {
91        for element in &import.imported_elements {
92            if element.element == "*" {
93                let mut fqn: String = String::new();
94                fqn.push_str(import.module.as_str());
95                fqn.push_str("::");
96                fqn.push_str(var_name);
97                if ctx.functions.contains(&fqn.as_str()) {
98                    return true;
99                }
100            } else if element.element == var_name || element.alias.as_deref() == Some(var_name) {
101                let mut fqn: String = String::new();
102                fqn.push_str(import.module.as_str());
103                fqn.push_str("::");
104                fqn.push_str(element.element.as_str());
105                if ctx.functions.contains(&fqn.as_str()) {
106                    return true;
107                }
108            }
109        }
110    }
111    false
112}
113
114fn validate_pel(
115    expression: &AstNode,
116    ctx: &InputContext,
117    imports: &DeclaredImports,
118    namespace: &DeclaredNamespaces,
119) -> Vec<Message> {
120    match expression {
121        AstNode::Name {
122            prefix, location, ..
123        } => {
124            let maybe_prefix: &Option<AstNode> = prefix;
125            match maybe_prefix {
126                None => {
127                    vec![]
128                }
129                Some(prefix_node) => {
130                    let prefix_string = prefix_node.get_value().unwrap();
131                    if !namespace.contains_prefix(&prefix_string) {
132                        vec![Message {
133                            message: format!(
134                                "Unable to resolve namespace prefix of : `{}`",
135                                &prefix_string
136                            ),
137                            kind: MessageKind::Error,
138                            location: Location {
139                                start: location.start,
140                                end: location.end,
141                            },
142                        }]
143                    } else {
144                        vec![]
145                    }
146                }
147            }
148        }
149        AstNode::VariableReference { reference, .. } => match reference.as_ref() {
150            AstNode::NameIdentifier { name, location } => {
151                if name.len() == 1 {
152                    let var_name = name.first().unwrap();
153                    if !ctx.inputs.contains(&var_name.as_str()) {
154                        if !is_imported(var_name, ctx, imports) {
155                            vec![Message {
156                                message: format!("Unable to resolve reference of: `{var_name}`."),
157                                kind: MessageKind::Error,
158                                location: Location {
159                                    start: location.start,
160                                    end: location.end,
161                                },
162                            }]
163                        } else {
164                            vec![]
165                        }
166                    } else {
167                        vec![]
168                    }
169                } else {
170                    let fqn = name.join("::");
171                    if !ctx.functions.contains(&fqn.as_str()) {
172                        vec![Message {
173                            message: format!("Unable to resolve module with identifier: `{fqn}`."),
174                            kind: MessageKind::Error,
175                            location: Location {
176                                start: location.start,
177                                end: location.end,
178                            },
179                        }]
180                    } else {
181                        vec![]
182                    }
183                }
184            }
185            _ => {
186                vec![]
187            }
188        },
189
190        AstNode::Array { items, .. } => items
191            .iter()
192            .flat_map(|node| validate_pel(node, ctx, imports, namespace))
193            .collect(),
194        AstNode::Comparison { lhs, rhs, .. } => [lhs, rhs]
195            .iter()
196            .flat_map(|node| validate_pel(node.as_ref(), ctx, imports, namespace))
197            .collect(),
198        AstNode::Logical { lhs, rhs, .. } => [lhs, rhs]
199            .iter()
200            .flat_map(|node| validate_pel(node.as_ref(), ctx, imports, namespace))
201            .collect(),
202        AstNode::Not { rhs, .. } => validate_pel(rhs, ctx, imports, namespace),
203        AstNode::ValueSelector { lhs, rhs, .. } => [lhs, rhs]
204            .iter()
205            .flat_map(|node| validate_pel(node.as_ref(), ctx, imports, namespace))
206            .collect(),
207        AstNode::InfixFunctionCall { lhs, func, rhs, .. } => [lhs, func, rhs]
208            .iter()
209            .flat_map(|node| validate_pel(node.as_ref(), ctx, imports, namespace))
210            .collect(),
211        AstNode::FunctionCall { lhs, args, .. } => {
212            let mut vec1 = args.clone();
213            vec1.push(lhs.as_ref().to_owned());
214            vec1.iter()
215                .flat_map(|node| validate_pel(node, ctx, imports, namespace))
216                .collect()
217        }
218        AstNode::IfElse {
219            condition,
220            if_branch,
221            else_branch,
222            ..
223        } => [condition, if_branch, else_branch]
224            .iter()
225            .flat_map(|node| validate_pel(node.as_ref(), ctx, imports, namespace))
226            .collect(),
227        AstNode::Default { lhs, rhs, .. } => [lhs, rhs]
228            .iter()
229            .flat_map(|node| validate_pel(node.as_ref(), ctx, imports, namespace))
230            .collect(),
231        AstNode::Document {
232            header: _header,
233            body,
234            ..
235        } => [body]
236            .iter()
237            .flat_map(|node| validate_pel(node.as_ref(), ctx, imports, namespace))
238            .collect(),
239        AstNode::Number {
240            value, location, ..
241        } => match value.parse::<f64>() {
242            Ok(_) => {
243                vec![]
244            }
245            Err(_) => {
246                vec![Message {
247                    message: format!("Number `{value}` should be a valid Double."),
248                    kind: MessageKind::Error,
249                    location: Location {
250                        start: location.start,
251                        end: location.end,
252                    },
253                }]
254            }
255        },
256        AstNode::Object { location, .. } => {
257            vec![Message {
258                message: "Object expressions are not yet supported.".to_string(),
259                kind: MessageKind::Error,
260                location: Location {
261                    start: location.start,
262                    end: location.end,
263                },
264            }]
265        }
266        AstNode::NullSafeSelector { lhs, .. } => {
267            validate_pel(lhs.as_ref(), ctx, imports, namespace)
268        }
269        n => n
270            .children()
271            .iter()
272            .flat_map(|node| validate_pel(node, ctx, imports, namespace))
273            .collect(),
274    }
275}
276
277fn collect_declared_ns(expression: &AstNode) -> DeclaredNamespaces {
278    match expression {
279        AstNode::Document { header: None, .. } => DeclaredNamespaces::default(),
280        AstNode::Document {
281            header: Some(directives),
282            ..
283        } => directives
284            .iter()
285            .map(collect_declared_ns)
286            .reduce(|acc, e| {
287                let x: Vec<NamespaceInformation> =
288                    [acc.namespaces.as_slice(), e.namespaces.as_slice()].concat();
289                DeclaredNamespaces { namespaces: x }
290            })
291            .unwrap_or_default(),
292        AstNode::NamespaceDirective { prefix, uri, .. } => DeclaredNamespaces {
293            namespaces: vec![NamespaceInformation {
294                prefix: prefix.get_value().unwrap(),
295                uri: uri.get_value().unwrap(),
296            }],
297        },
298        _ => DeclaredNamespaces::default(),
299    }
300}
301fn collect_declared_imports(expression: &AstNode) -> DeclaredImports {
302    match expression {
303        AstNode::Document { header: None, .. } => DeclaredImports::default(),
304        AstNode::Document {
305            header: Some(directives),
306            ..
307        } => directives
308            .iter()
309            .map(collect_declared_imports)
310            .reduce(|acc, e| {
311                let x: Vec<ImportDefinition> =
312                    [acc.imports.as_slice(), e.imports.as_slice()].concat();
313                DeclaredImports { imports: x }
314            })
315            .unwrap_or_default(),
316        AstNode::ImportDirective {
317            module, elements, ..
318        } => DeclaredImports {
319            imports: vec![ImportDefinition {
320                module: name_as_string(module.as_ref()),
321                alias: alias_as_string(&module.alias),
322                imported_elements: elements
323                    .iter()
324                    .map(|e| ImportElement {
325                        element: name_as_string(e),
326                        alias: alias_as_string(&e.alias),
327                    })
328                    .collect(),
329            }],
330        },
331        _ => DeclaredImports::default(),
332    }
333}
334
335fn name_as_string(module: &ImportedElement) -> String {
336    match module.name.as_ref() {
337        AstNode::NameIdentifier { name, .. } => name.join("::"),
338        _ => "".to_string(),
339    }
340}
341
342fn alias_as_string(option: &Option<AstNode>) -> Option<String> {
343    option.as_ref().map(|a| match a {
344        AstNode::NameIdentifier { name, .. } => name.join("::"),
345        _ => "".to_string(),
346    })
347}
348
349/// Compiles the provided DataWeave expression and returns a [CompilationResult].
350pub fn compile_to_pel_expr(
351    script_name: &str,
352    expression_str: &str,
353    ctx: &InputContext,
354) -> CompilationResult {
355    let result = parse_dw(script_name, expression_str);
356    match result {
357        Ok(parse_result) => {
358            let expression = parse_result.ast;
359            let declared_imports: DeclaredImports = collect_declared_imports(&expression);
360            let declared_ns: DeclaredNamespaces = collect_declared_ns(&expression);
361            let messages = validate_pel(&expression, ctx, &declared_imports, &declared_ns);
362            let errors = messages.iter().any(|m| m.kind == MessageKind::Error);
363            CompilationResult {
364                pel: (!errors).then(|| to_pel(&expression, &declared_ns)),
365                messages,
366            }
367        }
368        Err(e) => CompilationResult {
369            pel: None,
370            messages: e.messages,
371        },
372    }
373}
374
375fn to_pel(expression: &AstNode, declared_namespaces: &DeclaredNamespaces) -> String {
376    match expression {
377        AstNode::Str {
378            value: content,
379            quote,
380            location,
381        } => {
382            format!(
383                "[{}, {}, {}]",
384                str(":str"),
385                loc_str(location),
386                str(unescape_string(content.to_string(), quote).as_str())
387            )
388        }
389        AstNode::Boolean { value, location } => {
390            format!(
391                "[{}, {}, {}]",
392                str(":bool"),
393                loc_str(location),
394                str(value.to_string().as_str())
395            )
396        }
397        AstNode::Number { value, location } => {
398            format!(
399                "[{}, {}, {}]",
400                str(":nbr"),
401                loc_str(location),
402                str(value.to_string().as_str())
403            )
404        }
405        AstNode::Null { location } => {
406            format!("[{}, {}]", str(":null"), loc_str(location))
407        }
408        AstNode::DateTime { value, location } => {
409            format!(
410                "[{}, {}, {}]",
411                str(":datetime"),
412                loc_str(location),
413                str(value.as_str())
414            )
415        }
416        AstNode::LocalDateTime { value, location } => {
417            format!(
418                "[{}, {}, {}]",
419                str(":ldatetime"),
420                loc_str(location),
421                str(value.as_str())
422            )
423        }
424        AstNode::Array { items, location } => {
425            if items.is_empty() {
426                format!("[{}, {}]", str(":array"), loc_str(location))
427            } else {
428                let elements: Vec<String> = items
429                    .iter()
430                    .map(|a| to_pel(a, declared_namespaces))
431                    .collect();
432                let array_elements = elements.join(", ");
433                format!(
434                    "[{}, {}, {}]",
435                    str(":array"),
436                    loc_str(location),
437                    array_elements
438                )
439            }
440        }
441        AstNode::Comparison {
442            lhs,
443            rhs,
444            comp,
445            location,
446        } => {
447            let c = match comp {
448                ComparisonType::Greater => ">",
449                ComparisonType::Less => "<",
450                ComparisonType::Equal => "==",
451                ComparisonType::NotEqual => "!=",
452                ComparisonType::Similar => "~=",
453                ComparisonType::GreaterEqual => ">=",
454                _ => "<=",
455            };
456            format!(
457                "[{}, {}, {}, {}]",
458                str(c),
459                loc_str(location),
460                to_pel(lhs.as_ref(), declared_namespaces),
461                to_pel(rhs.as_ref(), declared_namespaces)
462            )
463        }
464        AstNode::Logical {
465            lhs,
466            rhs,
467            comp,
468            location,
469        } => {
470            let c = match comp {
471                LogicalType::And => "&&",
472                LogicalType::Or => "||",
473            };
474            format!(
475                "[{}, {}, {}, {}]",
476                str(c),
477                loc_str(location),
478                to_pel(lhs.as_ref(), declared_namespaces),
479                to_pel(rhs.as_ref(), declared_namespaces)
480            )
481        }
482        AstNode::Not { rhs: exp, location } => {
483            format!(
484                "[{}, {}, {}]",
485                str("!"),
486                loc_str(location),
487                to_pel(exp.as_ref(), declared_namespaces)
488            )
489        }
490        AstNode::ValueSelector { lhs, rhs, location } => {
491            format!(
492                "[{}, {}, {}, {}]",
493                str("."),
494                loc_str(location),
495                to_pel(lhs.as_ref(), declared_namespaces),
496                to_pel(rhs.as_ref(), declared_namespaces)
497            )
498        }
499        AstNode::AttributeSelector { lhs, rhs, location } => {
500            format!(
501                "[{}, {}, {}, {}]",
502                str("@"),
503                loc_str(location),
504                to_pel(lhs.as_ref(), declared_namespaces),
505                to_pel(rhs.as_ref(), declared_namespaces)
506            )
507        }
508        AstNode::MultiValueSelector { lhs, rhs, location } => {
509            format!(
510                "[{}, {}, {}, {}]",
511                str("*"),
512                loc_str(location),
513                to_pel(lhs.as_ref(), declared_namespaces),
514                to_pel(rhs.as_ref(), declared_namespaces)
515            )
516        }
517        AstNode::Name {
518            prefix,
519            value,
520            location,
521        } => {
522            let maybe_prefix: &Option<AstNode> = prefix;
523            match maybe_prefix {
524                None => {
525                    //For backwards compatibility
526                    to_pel(value, declared_namespaces)
527                }
528                Some(prefix) => {
529                    let prefix_str = &prefix.get_value().unwrap();
530                    let namespace = &declared_namespaces.resolve_prefix(prefix_str).unwrap();
531                    let ns_node = format!(
532                        "[{}, {}, {}]",
533                        str(":ns"),
534                        loc_str(&prefix.location()),
535                        str(namespace)
536                    );
537                    format!(
538                        "[{}, {}, {}, {}]",
539                        str(":name"),
540                        loc_str(location),
541                        ns_node,
542                        to_pel(value.as_ref(), declared_namespaces)
543                    )
544                }
545            }
546        }
547        AstNode::NameIdentifier {
548            name,
549            location: _location,
550        } => {
551            // TODO We need to execute the mapping between
552            str(name.last().unwrap().as_str())
553        }
554        AstNode::VariableReference {
555            reference,
556            location,
557        } => {
558            format!(
559                "[{}, {}, {}]",
560                str(":ref"),
561                loc_str(location),
562                to_pel(reference.as_ref(), declared_namespaces)
563            )
564        }
565        AstNode::InfixFunctionCall {
566            lhs,
567            func,
568            rhs,
569            location,
570        } => {
571            format!(
572                "[{}, {}, {}, {}, {}]",
573                str(":apply"),
574                loc_str(location),
575                to_pel(func.as_ref(), declared_namespaces),
576                to_pel(lhs.as_ref(), declared_namespaces),
577                to_pel(rhs.as_ref(), declared_namespaces)
578            )
579        }
580        AstNode::FunctionCall {
581            lhs,
582            args,
583            location,
584        } => {
585            if args.is_empty() {
586                format!(
587                    "[{}, {}, {}]",
588                    str(":apply"),
589                    loc_str(location),
590                    to_pel(lhs, declared_namespaces)
591                )
592            } else {
593                let elements: Vec<String> = args
594                    .iter()
595                    .map(|n| to_pel(n, declared_namespaces))
596                    .collect();
597                let array_elements = elements.join(", ");
598                format!(
599                    "[{}, {}, {}, {}]",
600                    str(":apply"),
601                    loc_str(location),
602                    to_pel(lhs, declared_namespaces),
603                    array_elements
604                )
605            }
606        }
607        AstNode::IfElse {
608            condition,
609            if_branch,
610            else_branch,
611            location,
612        } => {
613            format!(
614                "[{}, {}, {}, {}, {}]",
615                str(":if"),
616                loc_str(location),
617                to_pel(condition, declared_namespaces),
618                to_pel(if_branch, declared_namespaces),
619                to_pel(else_branch, declared_namespaces)
620            )
621        }
622        AstNode::Default { lhs, rhs, location } => {
623            format!(
624                "[{}, {}, {}, {}]",
625                str(":default"),
626                loc_str(location),
627                to_pel(lhs, declared_namespaces),
628                to_pel(rhs, declared_namespaces)
629            )
630        }
631        AstNode::Period { value, location } => {
632            format!(
633                "[{}, {}, {}]",
634                str(":period"),
635                loc_str(location),
636                str(value)
637            )
638        }
639        AstNode::Time { value, location } => {
640            format!("[{}, {}, {}]", str(":time"), loc_str(location), str(value))
641        }
642        AstNode::LocalTime { value, location } => {
643            format!("[{}, {}, {}]", str(":ltime"), loc_str(location), str(value))
644        }
645        AstNode::Date { value, location } => {
646            format!("[{}, {}, {}]", str(":ldate"), loc_str(location), str(value))
647        }
648        AstNode::NullSafeSelector { lhs, .. } => to_pel(lhs.as_ref(), declared_namespaces),
649        AstNode::Document { body, .. } => to_pel(body.as_ref(), declared_namespaces),
650        AstNode::StrInterpolation {
651            segments, location, ..
652        } => segments
653            .iter()
654            .map(|n| to_pel(n, declared_namespaces))
655            .reduce(|acc, value| {
656                format!(
657                    "[{}, {}, {}, {}]",
658                    str(":++"),
659                    loc_str(location),
660                    acc,
661                    value
662                )
663            })
664            .unwrap(),
665        _ => "".to_string(),
666    }
667}
668
669fn loc_str(location: &Location) -> String {
670    str(format!("{}-{}", location.start, location.end).as_str())
671}
672
673fn str(text: &str) -> String {
674    escape_str(text)
675}