pdk_script/
evaluator.rs

1// Copyright (c) 2025, 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
31/// An evaluator for [`Script`]s.
32pub struct Evaluator<'a> {
33    stream_solver: StreamSolver,
34    evaluation: Option<PelExpression>,
35    result: Option<PelValue>,
36    error: Option<RuntimeError>,
37
38    input_context: &'a InputContext,
39    transient_context: TransientContext,
40    vars: HashMap<String, Value>,
41
42    content_type: RefCell<Option<Format>>,
43}
44
45impl<'a> Evaluator<'a> {
46    pub(crate) fn new(
47        stream_solver: StreamSolver,
48        evaluation: Option<PelExpression>,
49        result: Option<PelValue>,
50        context: &'a InputContext,
51    ) -> Self {
52        Self {
53            stream_solver,
54            evaluation,
55            result,
56            error: None,
57            input_context: context,
58            transient_context: TransientContext::default(),
59            vars: HashMap::new(),
60            content_type: RefCell::new(None),
61        }
62    }
63
64    /// Binds a set of values to a variable indentified by the `name` argument,
65    /// nested into the `vars` namespace.
66    pub fn bind_vars<K: IntoValue>(&mut self, name: &str, value: K) {
67        if !self.is_ready() {
68            self.vars.insert(name.to_string(), value.into_value());
69            if self
70                .input_context
71                .vars()
72                .iter()
73                .all(|item| self.vars.contains_key(item))
74            {
75                let vars = self.vars.drain().collect();
76                self.do_eval(&VarsContext::new(self.input_context, vars));
77            }
78        }
79    }
80
81    /// Binds a payload to the `payload` variable.
82    pub fn bind_payload(&mut self, binding: &dyn PayloadBinding) {
83        if !self.is_ready() {
84            let content_type = *self.content_type.borrow();
85            self.do_eval(&PayloadContext::new(
86                self.input_context,
87                binding,
88                content_type,
89            ))
90        }
91    }
92
93    #[cfg(feature = "stream_body")]
94    /// Binds a payload to the `payload` variable.
95    pub async fn bind_stream_body(&mut self, body: BodyStream<'_>) {
96        if !self.is_ready() {
97            let content_type = *self.content_type.borrow();
98
99            let body = self.stream_solver.bind_body_stream(body).await;
100
101            self.do_eval(&PayloadContext::new(
102                self.input_context,
103                &body,
104                content_type,
105            ))
106        }
107    }
108
109    /// Binds a set of actual attributes to the `attributes` namespace.
110    pub fn bind_attributes(&mut self, binding: &dyn AttributesBinding) {
111        if !self.is_ready() {
112            Self::extract_format(binding)
113                .map(|content_type| self.content_type.borrow_mut().replace(content_type));
114
115            self.stream_solver.bind_attributes(binding);
116            let adapter = AttributesBindingAdapter::new(binding);
117            self.do_eval(&AttributesContext::new(self.input_context, &adapter))
118        }
119    }
120
121    /// Binds a set of actual authentication properties to the `authentication` namespace.
122    pub fn bind_authentication(&mut self, binding: &dyn AuthenticationBinding) {
123        if !self.is_ready() {
124            self.do_eval(&AuthenticationContext::new(self.input_context, binding))
125        }
126    }
127
128    fn extract_format(binding: &dyn AttributesBinding) -> Option<Format> {
129        binding.extract_header("content-type").and_then(|header| {
130            if header.contains("/xml") {
131                Some(Format::Xml)
132            } else if header.contains("/json") {
133                Some(Format::Json)
134            } else if header.contains("text/plain") {
135                Some(Format::PlainText)
136            } else {
137                None
138            }
139        })
140    }
141
142    fn do_eval(&mut self, bound_context: &'_ dyn Context) {
143        let local_context = MergedContext::new(self.input_context, &self.transient_context);
144        let merged_context = MergedContext::new(&local_context, bound_context);
145        if let Some(expr) = self.evaluation.take() {
146            match Runtime::new().eval_with_context(&expr, &merged_context) {
147                Ok(eval) => match eval {
148                    Evaluation::Complete(_, v) => {
149                        self.result = Some(v);
150                    }
151                    Evaluation::Partial(exp) => {
152                        self.transient_context.detach_pending(&exp, bound_context);
153                        self.evaluation = Some(exp);
154                    }
155                },
156                Err(error) => {
157                    self.error = Some(error);
158                }
159            }
160        }
161    }
162
163    /// Returns `[true]` if the Evaluator is ready to be evaluated.
164    pub fn is_ready(&self) -> bool {
165        self.result.is_some() || self.error.is_some()
166    }
167
168    /// Evals the script with the given bindings and returns the resulting [`Value`].
169    /// Returns an [`EvaluationError`] when errors occurs during the evaluation.
170    pub fn eval(self) -> Result<Value, EvaluationError> {
171        if let Some(error) = self.error {
172            return Err(EvaluationError::Error(error));
173        }
174
175        self.result
176            .ok_or(EvaluationError::PartialEvaluation)
177            .and_then(|val| Value::try_from(&val))
178    }
179}
180
181/// Represents an evaluation error.
182#[derive(Error, Debug)]
183pub enum EvaluationError {
184    /// Represents an incomplete evaluation error.
185    #[error("Evaluation is incomplete")]
186    PartialEvaluation,
187
188    /// Represents a type mismatch error.
189    #[error("Unsupported DataType")]
190    TypeMismatch,
191
192    /// Represents a runtime error.
193    #[error("Error evaluating expression: {0}")]
194    Error(RuntimeError),
195}