Skip to main content

pdk_script/
evaluator.rs

1// Copyright (c) 2026, Salesforce, Inc.,
2// All rights reserved.
3// For full license text, see the LICENSE.txt file
4
5use crate::bindings::attributes::AttributesBindingAdapter;
6use crate::context::attributes::AttributesContext;
7use crate::context::authentication::AuthenticationContext;
8use crate::context::input::InputContext;
9use crate::context::merged::MergedContext;
10use crate::context::payload::PayloadContext;
11use crate::context::reference::TransientContext;
12use crate::context::vars::VarsContext;
13use crate::context::StreamSolver;
14use crate::value::IntoValue;
15use crate::{AttributesBinding, AuthenticationBinding, Format, PayloadBinding, Value};
16use pel::expression::Expression as PelExpression;
17use pel::runtime::value::Value as PelValue;
18use pel::runtime::{Context, Evaluation, Runtime, RuntimeError};
19use std::cell::RefCell;
20use std::collections::HashMap;
21use std::convert::TryFrom;
22use thiserror::Error;
23
24#[cfg(feature = "stream_body")]
25use classy::hl::BodyStream;
26
27// For rustdoc.
28#[allow(unused)]
29use crate::Script;
30
31thread_local! {
32    // Runtime only holds the immutable PEL prelude. Reusing it avoids rebuilding
33    // the built-in function map for every expression evaluation.
34    static RUNTIME: Runtime = Runtime::new();
35}
36
37/// An evaluator for [`Script`]s.
38pub struct Evaluator<'a> {
39    stream_solver: StreamSolver,
40    evaluation: Option<PelExpression>,
41    result: Option<PelValue>,
42    error: Option<RuntimeError>,
43
44    input_context: &'a InputContext,
45    transient_context: TransientContext,
46    vars: Option<HashMap<String, Value>>,
47
48    content_type: RefCell<Option<Format>>,
49}
50
51impl<'a> Evaluator<'a> {
52    pub(crate) fn new(
53        stream_solver: StreamSolver,
54        evaluation: Option<PelExpression>,
55        result: Option<PelValue>,
56        context: &'a InputContext,
57    ) -> Self {
58        Self {
59            stream_solver,
60            evaluation,
61            result,
62            error: None,
63            input_context: context,
64            transient_context: TransientContext::default(),
65            vars: (!context.vars().is_empty()).then(HashMap::new),
66            content_type: RefCell::new(None),
67        }
68    }
69
70    /// Binds a set of values to a variable indentified by the `name` argument,
71    /// nested into the `vars` namespace.
72    pub fn bind_vars<K: IntoValue>(&mut self, name: &str, value: K) {
73        if !self.is_ready() {
74            let vars = self
75                .vars
76                .as_mut()
77                .expect("variables are only bound by scripts that declare them");
78            vars.insert(name.to_string(), value.into_value());
79            if self
80                .input_context
81                .vars()
82                .iter()
83                .all(|item| vars.contains_key(item))
84            {
85                let vars = std::mem::take(vars);
86                self.do_eval(&VarsContext::new(self.input_context, vars));
87            }
88        }
89    }
90
91    /// Binds a payload to the `payload` variable.
92    pub fn bind_payload(&mut self, binding: &dyn PayloadBinding) {
93        if !self.is_ready() {
94            let content_type = *self.content_type.borrow();
95            self.do_eval(&PayloadContext::new(
96                self.input_context,
97                binding,
98                content_type,
99            ))
100        }
101    }
102
103    #[cfg(feature = "stream_body")]
104    /// Binds a payload to the `payload` variable.
105    pub async fn bind_stream_body(&mut self, body: BodyStream<'_>) {
106        if !self.is_ready() {
107            let content_type = *self.content_type.borrow();
108
109            let body = self.stream_solver.bind_body_stream(body).await;
110
111            self.do_eval(&PayloadContext::new(
112                self.input_context,
113                &body,
114                content_type,
115            ))
116        }
117    }
118
119    /// Binds a set of actual attributes to the `attributes` namespace.
120    pub fn bind_attributes(&mut self, binding: &dyn AttributesBinding) {
121        if !self.is_ready() {
122            // Payload parsing needs request metadata, while attribute-only scripts do not.
123            // InputContext is derived during script compilation, so this remains correct for
124            // every expression that declares payload access.
125            if !self.input_context.format().is_empty() {
126                Self::extract_format(binding)
127                    .map(|content_type| self.content_type.borrow_mut().replace(content_type));
128
129                self.stream_solver.bind_attributes(binding);
130            }
131            let adapter = AttributesBindingAdapter::new(binding);
132            self.do_eval(&AttributesContext::new(self.input_context, &adapter))
133        }
134    }
135
136    /// Binds a set of actual authentication properties to the `authentication` namespace.
137    pub fn bind_authentication(&mut self, binding: &dyn AuthenticationBinding) {
138        if !self.is_ready() {
139            self.do_eval(&AuthenticationContext::new(self.input_context, binding))
140        }
141    }
142
143    fn extract_format(binding: &dyn AttributesBinding) -> Option<Format> {
144        binding.extract_header("content-type").and_then(|header| {
145            if header.contains("/xml") {
146                Some(Format::Xml)
147            } else if header.contains("/json") {
148                Some(Format::Json)
149            } else if header.contains("text/plain") {
150                Some(Format::PlainText)
151            } else {
152                None
153            }
154        })
155    }
156
157    fn do_eval(&mut self, bound_context: &'_ dyn Context) {
158        let local_context = MergedContext::new(self.input_context, &self.transient_context);
159        let merged_context = MergedContext::new(&local_context, bound_context);
160        if let Some(expr) = self.evaluation.take() {
161            let evaluation =
162                RUNTIME.with(|runtime| runtime.eval_with_context(&expr, &merged_context));
163            match evaluation {
164                Ok(eval) => match eval {
165                    Evaluation::Complete(_, v) => {
166                        self.result = Some(v);
167                    }
168                    Evaluation::Partial(exp) => {
169                        self.transient_context.detach_pending(&exp, bound_context);
170                        self.evaluation = Some(exp);
171                    }
172                },
173                Err(error) => {
174                    self.error = Some(error);
175                }
176            }
177        }
178    }
179
180    /// Returns `[true]` if the Evaluator is ready to be evaluated.
181    pub fn is_ready(&self) -> bool {
182        self.result.is_some() || self.error.is_some()
183    }
184
185    /// Evals the script with the given bindings and returns the resulting [`Value`].
186    /// Returns an [`EvaluationError`] when errors occurs during the evaluation.
187    pub fn eval(self) -> Result<Value, EvaluationError> {
188        if let Some(error) = self.error {
189            return Err(EvaluationError::Error(error));
190        }
191
192        self.result
193            .ok_or(EvaluationError::PartialEvaluation)
194            .and_then(|val| Value::try_from(&val))
195    }
196}
197
198/// Represents an evaluation error.
199#[derive(Error, Debug)]
200pub enum EvaluationError {
201    /// Represents an incomplete evaluation error.
202    #[error("Evaluation is incomplete")]
203    PartialEvaluation,
204
205    /// Represents a type mismatch error.
206    #[error("Unsupported DataType")]
207    TypeMismatch,
208
209    /// Represents a runtime error.
210    #[error("Error evaluating expression: {0}")]
211    Error(RuntimeError),
212}