pdk-script 1.3.0-alpha.0

PDK Script
Documentation
// Copyright 2023 Salesforce, Inc. All rights reserved.

use crate::bindings::attributes::AttributesBindingAdapter;
use crate::context::attributes::AttributesContext;
use crate::context::authentication::AuthenticationContext;
use crate::context::input::InputContext;
use crate::context::merged::MergedContext;
use crate::context::payload::PayloadContext;
use crate::context::reference::TransientContext;
use crate::context::vars::VarsContext;
use crate::value::IntoValue;
use crate::{AttributesBinding, AuthenticationBinding, PayloadBinding, Value};
use pel::expression::Expression as PelExpression;
use pel::runtime::value::Value as PelValue;
use pel::runtime::{Context, Evaluation, Runtime, RuntimeError};
use std::collections::HashMap;
use std::convert::TryFrom;
use thiserror::Error;

// For rustdoc.
#[allow(unused)]
use crate::Script;

/// An evaluator for [`Script`]s.
pub struct Evaluator<'a> {
    evaluation: Option<PelExpression>,
    result: Option<PelValue>,
    error: Option<RuntimeError>,

    input_context: &'a InputContext,
    transient_context: TransientContext,
    vars: HashMap<String, Value>,
}

impl<'a> Evaluator<'a> {
    pub(crate) fn new(
        evaluation: Option<PelExpression>,
        result: Option<PelValue>,
        context: &'a InputContext,
    ) -> Self {
        Self {
            evaluation,
            result,
            error: None,
            input_context: context,
            transient_context: TransientContext::default(),
            vars: HashMap::new(),
        }
    }

    /// Binds a set of values to a variable indentified by the `name` argument,
    /// nested into the `vars` namespace.
    pub fn bind_vars<K: IntoValue>(&mut self, name: &str, value: K) {
        if !self.is_ready() {
            self.vars.insert(name.to_string(), value.into_value());
            if self
                .input_context
                .vars()
                .iter()
                .all(|item| self.vars.contains_key(item))
            {
                let vars = self.vars.drain().collect();
                self.do_eval(&VarsContext::new(self.input_context, vars));
            }
        }
    }

    /// Binds a payload to the `payload` variable.
    pub fn bind_payload(&mut self, binding: &dyn PayloadBinding) {
        if !self.is_ready() {
            self.do_eval(&PayloadContext::new(self.input_context, binding))
        }
    }

    /// Binds a set of actual attributes to the `attributes` namespace.
    pub fn bind_attributes(&mut self, binding: &dyn AttributesBinding) {
        if !self.is_ready() {
            let adapter = AttributesBindingAdapter::new(binding);
            self.do_eval(&AttributesContext::new(self.input_context, &adapter))
        }
    }

    /// Binds a set of actual authentication properties to the `authentication` namespace.
    pub fn bind_authentication(&mut self, binding: &dyn AuthenticationBinding) {
        if !self.is_ready() {
            self.do_eval(&AuthenticationContext::new(self.input_context, binding))
        }
    }

    fn do_eval(&mut self, bound_context: &'_ dyn Context) {
        let local_context = MergedContext::new(self.input_context, &self.transient_context);
        let merged_context = MergedContext::new(&local_context, bound_context);
        if let Some(expr) = self.evaluation.take() {
            match Runtime::new().eval_with_context(&expr, &merged_context) {
                Ok(eval) => match eval {
                    Evaluation::Complete(_, v) => {
                        self.result = Some(v);
                    }
                    Evaluation::Partial(exp) => {
                        self.transient_context.detach_pending(&exp, bound_context);
                        self.evaluation = Some(exp);
                    }
                },
                Err(error) => {
                    self.error = Some(error);
                }
            }
        }
    }

    /// Returns `[true]` if the Evaluator is ready to be evaluated.
    pub fn is_ready(&self) -> bool {
        self.result.is_some() || self.error.is_some()
    }

    /// Evals the script with the given bindings and returns the resulting [`Value`].
    /// Returns an [`EvaluationError`] when errors occurs during the evaluation.
    pub fn eval(self) -> Result<Value, EvaluationError> {
        if let Some(error) = self.error {
            return Err(EvaluationError::Error(error));
        }

        self.result
            .ok_or(EvaluationError::PartialEvaluation)
            .and_then(|val| Value::try_from(&val))
    }
}

/// Represents an evaluation error.
#[derive(Error, Debug)]
pub enum EvaluationError {
    /// Represents an incomplete evaluation error.
    #[error("Evaluation is incomplete")]
    PartialEvaluation,

    /// Represents a type mismatch error.
    #[error("Unsupported DataType")]
    TypeMismatch,

    /// Represents a runtime error.
    #[error("Error evaluating expression: {0}")]
    Error(RuntimeError),
}